目录

  • 摘要
  • 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 绑定的更多相关文章

  1. KnockoutJS 3.X API 第四章 表单绑定(11) options绑定

    目的 options绑定主要用于下拉列表中(即<select>元素)或多选列表(例如,<select size='6'>).此绑定不能与除<select>元素之外的 ...

  2. Knockout.Js官网学习(options绑定)

    前言 options绑定控制什么样的options在drop-down列表里(例如:<select>)或者 multi-select 列表里 (例如:<select size='6' ...

  3. KnockoutJS Select 标签 Options绑定

    <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title&g ...

  4. knockoutJS学习笔记08:表单域绑定

    前面的绑定都是用在基本标签上,这章主要讲表单域标签的绑定. 一.value 绑定 绑定标签:input text.textarea. <p>用户名:<input type=" ...

  5. Knockout.Js官网学习(value绑定)

    前言 value绑定是关联DOM元素的值到view model的属性上.主要是用在表单控件<input>,<select>和<textarea>上. 当用户编辑表单 ...

  6. Knockoutjs 实践入门 (3) 绑定数组

    <form id="form1" runat="server">        <div>            <!--text ...

  7. KnockoutJS 3.X API 第四章 表单绑定(9) value绑定

    目的 value绑定主要用于DOM元素给视图模型赋值用的.通常用于<input><select><textarea>等元素. value绑定与text绑定的区别在于 ...

  8. KnockoutJS 3.X API 第四章 表单绑定(12) selectedOptions、uniqueName绑定

    selectedOptions绑定目的 selectedOptions绑定控制当前选择多选列表中的哪些元素. 这旨在与<select>元素和选项绑定结合使用. 当用户选择或取消选择多选列表 ...

  9. 4.Knockout.Js(事件绑定)

    前言 click绑定在DOM元素上添加事件句柄以便元素被点击的时候执行定义的JavaScript 函数.大部分是用在button,input和连接a上,但是可以在任意元素上使用. 简单示例 <h ...

随机推荐

  1. List.Sum…统计信息(Power Query 之 M 语言)

    数据源: 任意数据源,一列数值,一列非数值(文本) 目标: 对数值列进行求和等计算,对非数值列进行计数等计算 操作过程: 选取待计算的列>[转换]>[统计信息]>选取   M公式: ...

  2. Table.RenameColumns重命名…Rename…(Power Query 之 M 语言)

    数据源: "姓名""基数"等列 目标: 修改"姓名"列标题为"员工姓名" 操作过程: [转换]>[重命名]> ...

  3. java 多线程: Thread 锁ReentrantLock 常用方法

    注意 reentrantLock 和 同步代码块儿一样,同一个时刻只能一个线程获取锁,也就是共用锁的代码块部分,线程是顺序执行的 ReentrantLock 常用方法 getHoldCount获取当前 ...

  4. MLNX网卡驱动安装

    安装/升级MLNX驱动 1. 安装准备 驱动下载地址:https://www.mellanox.com/products/ethernet-drivers/linux/mlnx_en 选择和系统版本匹 ...

  5. SpringBoot项目使用Caffeine本地缓存

    环境配置:(或以上版本,必须) JDK 版本:1.8  Caffeine 版本:2.8.0SpringBoot 版本:2.2.2.RELEASE 也可以不与SpringBoot结合 1.添加maven ...

  6. SpringBoot整合Elasticsearch启动报错处理 nested exception is java.lang.IllegalStateException: availableProcessors is already set to [8], rejecting [8]

    Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean wit ...

  7. 【LeetCode】997. Find the Town Judge 解题报告(C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 度 日期 题目地址:https://leetcode ...

  8. 【LeetCode】561. Array Partition I 解题报告(Java & Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 排序 日期 题目地址:https://leetcod ...

  9. 【LeetCode】649. Dota2 Senate 解题报告(Python)

    [LeetCode]649. Dota2 Senate 解题报告(Python) 作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 题目地 ...

  10. 【LeetCode】481. Magical String 解题报告(Python)

    [LeetCode]481. Magical String 解题报告(Python) 标签(空格分隔): LeetCode 作者: 负雪明烛 id: fuxuemingzhu 个人博客: http:/ ...