使用 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 ...
随机推荐
- 我在这里的处女篇(Word技巧集团)
传说这里的文章可以在Word上打好了发布,Word嘛,有[听写]功能,不用打字了: 写好的文章还可以[大声朗读],边听边看最容易找"通假字"了. 冲这,我的新阵地就定这里了,哈哈~
- Swagger如何匹配多个Controller类或者目录
方法一(最普通的方式):匹配一个controller目录下的所有controller类. 1 @Bean 2 public Docket creatRestApi(){ 3 return new Do ...
- ICCV2021 | Tokens-to-Token ViT:在ImageNet上从零训练Vision Transformer
前言 本文介绍一种新的tokens-to-token Vision Transformer(T2T-ViT),T2T-ViT将原始ViT的参数数量和MAC减少了一半,同时在ImageNet上从 ...
- Django ModelForm表单验证
ModelForm 在使用Model和Form时,都需要对字段进行定义并指定类型,通过ModelForm则可以省去From中字段的定义 应用场景:定制model admin 的时候可以使用.适用于小业 ...
- Django 中间件理解
中间件 django 中的中间件(middleware),在django中,中间件其实就是一个类,在请求到来和结束后,django会根据自己的规则在合适的时机执行中间件中相应的方法. 应用场景,对所有 ...
- ACwing1216. 饮料换购
题目: 乐羊羊饮料厂正在举办一次促销优惠活动.乐羊羊C型饮料,凭3个瓶盖可以再换一瓶C型饮料,并且可以一直循环下去(但不允许暂借或赊账). 请你计算一下,如果小明不浪费瓶盖,尽量地参加活动,那么,对于 ...
- UDP&串口调试助手用法(4)
接收配置用法 概览 保存文件 可将数据保存到文件和文件夹 如果选择的时文件,则需要自己手动选择保存的文件. 如果选择的时文件夹,则需要指定文件夹的类型和文件的后缀 支持保存文件类型: 文本文件和二进制 ...
- VS c/c++常用配置项
VS2015 下面的配置,Vs是通用的 自己常用VS2015, 但其默认的一些设置不能满足我的日常. 比较熟悉c/c++, 以下配置仅适用c/c++ 设置方法: 工具-选项-文本编辑器-c/c++ 常 ...
- 【LeetCode】976. Largest Perimeter Triangle 解题报告(Python)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 排序 日期 题目地址:https://leetcod ...
- Anti-prime Sequences
Anti-prime Sequences Time Limit: 3000MS Memory Limit: 30000K Total Submissions: 3355 Accepted: 1 ...