使用 SourceGenerator 简化 Options 绑定
目录
- 摘要
- Options 绑定
- 使用 SourceGenerator 简化
- 如何 Debug SourceGenerator
- 如何 Format 生成的代码
- 使用方法
- SourceCode && Nuget package
- 总结
摘要
Never send a human to do a machine's job
Source Generator 随着 .net 5.0 推出,并在 .net 6 中大量使用,利用 SourceGenerator 可以将开发人员从一些模板化的重复的工作中解放出来,更多的投入创造力的工作。 一些开发人员可能会认为 SourceGenerator 很复杂,害怕去了解学习,我们将打破这种既定印象,不管其内部如何复杂,但至少使用起来很简单。
本系列将自顶向下的学习分享 SourceGenerator,先学习 SourceGenerator 如何在我们工作中应用,再逐渐深入学习其原理。本文将介绍如何使用 SourceGenerator 来自动将 Options 和 Configuration 绑定。
1. Options 绑定
一般情况下,Options 和 Configuration 的绑定关系我们使用如下代码来实现,其中只有 Options type 和 section key 会变化,其它部分都是重复的模板代码。
在之前的方案中我们可以想到的是在 Options 类打上一个注解,并在注解中指明 section key,然后在程序启动时,然后通过扫描程序集和反射在运行时动态调用 Configure 方法,但这样会有一定的运行时开销,拖慢启动速度。下面将介绍如何使用 SourceGenerator 在编译时解决问题。
builder.Services.Configure<GreetOption>(builder.Configuration.GetSection("Greet"));
2. 使用 SourceGenerator 简化
编译时代码生成需要足够多的元数据 (metadata),我们可以使用注解,命名,继承某个特定类,实现特定接口等途径来指明哪些东西需要生成或指明生成所需要的信息。在本文中我们想在编译时生成代码也必须知道 Options 类型和 section key,这里我们使用注解来提供元数据。
2.1 Option Attribute
被标记的 class 即为 Options 类型,构造函数参数即指明 section key
/// <summary>
/// Mark a class with a Key in IConfiguration which will be source generated in the DependencyInjection extension method
/// </summary>
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class OptionAttribute : Attribute
{
/// <summary>
/// The Key represent IConfiguration section
/// </summary>
public string Key { get; }
public OptionAttribute(string key)
{
Key = key;
}
}
并在需要绑定的 Options 类上边加上该 Attribute
[Option("Greet")]
public class GreetOption
{
public string Text { get; set; }
}
2.2 Options.SourceGenerator
新建 Options.SourceGenerator 项目,SourceGenerator 需要引入 Microsoft.CodeAnalysis.Analyzers, Microsoft.CodeAnalysis.CSharp 包,并将 TargetFramework 设置成 netstandard2.0。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IncludeBuildOutput>false</IncludeBuildOutput>
...
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
...
</ItemGroup>
</Project>
要使用 SourceGenerator 需要实现 ISourceGenerator 接口,并添加 [Generator] 注解,一般情况下我们在 Initialize 注册 Syntax receiver,将需要的类添加到接收器中,在 Execute 丢弃掉不是该接收器的上下文,执行具体的代码生成逻辑。
public interface ISourceGenerator
{
void Initialize(GeneratorInitializationContext context);
void Execute(GeneratorExecutionContext context);
}
这里我们需要了解下 roslyn api 中的 语法树模型 (SyntaxTree model) 和 语义模型 (Semantic model),简单的讲, 语法树表示源代码的语法和词法结构,表明节点是接口声明还是类声明还是 using 指令块等等,这一部分信息来源于编译器的 Parse 阶段;语义来源于编译器的 Declaration 阶段,由一系列 Named symbol 构成,比如 TypeSymbol,MethodSymbol 等,类似于 CLR 类型系统, TypeSymbol 可以得到标记的注解信息,MethodSymbol 可以得到 ReturnType 等信息。
定义 Options Syntax Receiver,这里我们处理节点信息是类声明语法的节点,并且类声明语法上有注解,然后再获取其语义模型,根据语义模型判断是否包含我们上边定义的 OptionAttribute
class OptionsSyntax : ISyntaxContextReceiver
{
public List<ITypeSymbol> TypeSymbols { get; set; } = new List<ITypeSymbol>();
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
if (context.Node is ClassDeclarationSyntax cds && cds.AttributeLists.Count > 0)
{
ITypeSymbol typeSymbol = context.SemanticModel.GetDeclaredSymbol(cds) as ITypeSymbol;
if (typeSymbol!.GetAttributes().Any(x =>
x.AttributeClass!.ToDisplayString() ==
"SourceGeneratorPower.Options.OptionAttribute"))
{
TypeSymbols.Add(typeSymbol);
}
}
}
}
接下来就是循环处理接收器中的 TypeSymbol,获取 OptionAttribute 的 AttributeData,一般通过构造函数初始化的 Attribute,是取 ConstructorArguments,而通过属性赋值的是取 NamedArguments,这里为了避免 using 问题直接取 typeSymbol 的 DisplayString 即包含了 Namespace 的类全名。并用这些元数据来生成对应的模板代码。
private string ProcessOptions(ISymbol typeSymbol, ISymbol attributeSymbol)
{
AttributeData attributeData = typeSymbol.GetAttributes()
.Single(ad => ad.AttributeClass!.Equals(attributeSymbol, SymbolEqualityComparer.Default));
TypedConstant path = attributeData.ConstructorArguments.First();
return $@"services.Configure<{typeSymbol.ToDisplayString()}>(configuration.GetSection(""{path.Value}""));";
}
由于 SourceGenerator 被设计成不能修改现有的代码,这里我们使用 SourceGenerator 来生成一个扩展方法,并将上边生成的模板代码添加进去。可以看见有一部分的代码是不会有变动的,这里有个小技巧,先写真实的类来发现其中的变化量,然后将不变的直接复制过来,而变化的部分再去动态拼接,注意收尾的括号不能少。
public void Execute(GeneratorExecutionContext context)
{
if (!(context.SyntaxContextReceiver is OptionsSyntax receiver))
{
return;
}
INamedTypeSymbol attributeSymbol =
context.Compilation.GetTypeByMetadataName("SourceGeneratorPower.Options.OptionAttribute");
StringBuilder source = new StringBuilder($@"
using Microsoft.Extensions.Configuration;
namespace Microsoft.Extensions.DependencyInjection
{{
public static class ScanInjectOptions
{{
public static void AutoInjectOptions(this IServiceCollection services, IConfiguration configuration)
{{
");
foreach (ITypeSymbol typeSymbol in receiver.TypeSymbols)
{
source.Append(' ', 12);
source.AppendLine(ProcessOptions(typeSymbol, attributeSymbol));
}
source.Append(' ', 8).AppendLine("}")
.Append(' ', 4).AppendLine("}")
.AppendLine("}");
context.AddSource("Options.AutoGenerated.cs",
SourceText.From(source.ToString(), Encoding.UTF8));
}
如何 Debug SourceGenerator
在写 SourceGenerator 的过程中,我们可能需要用到 Debug 功能,这里我们使用 Debugger 结合附加到进程进行 Debug,选择的进程名字一般是 csc.dll,注意需要提前打好断点,之前编译过还需要 Clean Solution。
一般在方法的开头我们加上以下代码,这样编译程序将一直自旋等待附加到进程。
if (!Debugger.IsAttached)
{
SpinWait.SpinUntil(() => Debugger.IsAttached);
}
如何 Format 生成的代码
可以看见上边的示例中,我们使用手动添加空格的方式来格式化代码,当需要生成的代码很多时,结构比较复杂时,我们如何格式化生成的代码呢?这里我们可以使用 CSharpSyntaxTree 来转换一下,再将格式化后的代码添加到编译管道中去。
var extensionTextFormatted = CSharpSyntaxTree.ParseText(extensionSource.ToString(), new CSharpParseOptions(LanguageVersion.CSharp8)).GetRoot().NormalizeWhitespace().SyntaxTree.GetText().ToString();
context.AddSource($"Options.AutoGenerated.cs", SourceText.From(extensionTextFormatted, Encoding.UTF8));
使用方法
首先在 Options 类上边打上标记
[Option("Greet")]
public class GreetOption
{
public string Text { get; set; }
}
appsetting.json 配置
{
"Greet": {
"Text": "Hello world!"
}
}
然后使用扩展方法, 这里以 .Net 6 为例, .Net5 也是类似的
builder.Services.AutoInjectOptions(builder.Configuration);
SourceCode && Nuget package
SourceCode: https://github.com/huiyuanai709/SourceGeneratorPower
Nuget Package: https://www.nuget.org/packages/SourceGeneratorPower.Options.Abstractions
Nuget Package: https://www.nuget.org/packages/SourceGeneratorPower.Options.SourceGenerator
总结
本文介绍了 Options Pattarn 与 Configuration 绑定的 SourceGenerator 实现,以及介绍了如何 Debug,和如何格式化代码。可以看见 SourceGenerator 使用起来也比较简单,套路不多,更多信息可以从官方文档
https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/ 及 Github 上了解和学习。
文章源自公众号:灰原同学的笔记,转载请联系授权
使用 SourceGenerator 简化 Options 绑定的更多相关文章
- KnockoutJS 3.X API 第四章 表单绑定(11) options绑定
目的 options绑定主要用于下拉列表中(即<select>元素)或多选列表(例如,<select size='6'>).此绑定不能与除<select>元素之外的 ...
- Knockout.Js官网学习(options绑定)
前言 options绑定控制什么样的options在drop-down列表里(例如:<select>)或者 multi-select 列表里 (例如:<select size='6' ...
- KnockoutJS Select 标签 Options绑定
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title&g ...
- knockoutJS学习笔记08:表单域绑定
前面的绑定都是用在基本标签上,这章主要讲表单域标签的绑定. 一.value 绑定 绑定标签:input text.textarea. <p>用户名:<input type=" ...
- Knockout.Js官网学习(value绑定)
前言 value绑定是关联DOM元素的值到view model的属性上.主要是用在表单控件<input>,<select>和<textarea>上. 当用户编辑表单 ...
- Knockoutjs 实践入门 (3) 绑定数组
<form id="form1" runat="server"> <div> <!--text ...
- KnockoutJS 3.X API 第四章 表单绑定(9) value绑定
目的 value绑定主要用于DOM元素给视图模型赋值用的.通常用于<input><select><textarea>等元素. value绑定与text绑定的区别在于 ...
- KnockoutJS 3.X API 第四章 表单绑定(12) selectedOptions、uniqueName绑定
selectedOptions绑定目的 selectedOptions绑定控制当前选择多选列表中的哪些元素. 这旨在与<select>元素和选项绑定结合使用. 当用户选择或取消选择多选列表 ...
- 4.Knockout.Js(事件绑定)
前言 click绑定在DOM元素上添加事件句柄以便元素被点击的时候执行定义的JavaScript 函数.大部分是用在button,input和连接a上,但是可以在任意元素上使用. 简单示例 <h ...
随机推荐
- 突出显示(Project)
<Project2016 企业项目管理实践>张会斌 董方好 编著 当一个大的项目文件做好以后,查看全部内容,肉眼多少会有点吃不消,这时就需要"划重点".在Porect里 ...
- SpringCloud微服务实战——搭建企业级开发框架(三十四):SpringCloud + Docker + k8s实现微服务集群打包部署-Maven打包配置
SpringCloud微服务包含多个SpringBoot可运行的应用程序,在单应用程序下,版本发布时的打包部署还相对简单,当有多个应用程序的微服务发布部署时,原先的单应用程序部署方式就会显得复杂且 ...
- Python3.6+Django2.0以上 xadmin站点的配置和使用
1. xadmin的介绍 django自带的admin站点虽然功能强大,但是界面不是很好看.而xadmin界面好看,功能更强大,并完全支持Bootstrap主题模板.xadmin内置了丰富的插件功能. ...
- listitems.ListItemCollectionPosition属性为空
SPListItemCollection listitems = list1.GetItems(query);//当执行完上面的代码后,listitems.ListItemCollectionPosi ...
- action中redirectAction到另一个命名空间中的action该如何配置
action中redirectAction到另一个命名空间中的action该如何配置,请注意namespace这儿必须是/global,而不是global,要不然找不到此action的
- SQL优化一例:通过改变分组条件(减少计算次数)来提高效率
#与各人授权日期相关,所以有十万用户,就有十万次查询(相关子查询) @Run.ExecuteSql("更新各人应听正课数",@"update bi_data.study_ ...
- redis启动报错 var/run/redis_6379.pid exists, process is already running or crashed
redis启动显示 /var/run/redis_6379.pid exists, process is already running or crashed 出现这个执行 rm -rf /var/r ...
- java源码——文件读写和单词统计
本文要解决的问题:"键盘输入一段英语语句,将这段话写入content.txt中,然后输出这段话,并且统计语句中英文单词的数目以及各个单词出现的次数." 分析问题知,核心是文件读写和 ...
- 【LeetCode】808. Soup Servings 解题报告(Python)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 题目地址:https://leetcode.com/problems/soup-serv ...
- 【剑指Offer】丑数 解题报告
[剑指Offer]丑数 解题报告(Python) 标签(空格分隔): 剑指Offer 题目地址:https://www.nowcoder.com/ta/coding-interviews 题目描述: ...