目录

  • 摘要
  • 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. CF1438A Specific Tastes of Andre 题解

    Content 如果一个序列的和能够被它的长度整除,我们称这个序列是不错的.如果一个序列的所有的非空子序列都是不错的,我们就称这个序列是完美的.现在有 \(t\) 组询问,每组询问给定一个整数 \(n ...

  2. 贪心——122.买卖股票的最佳时机II

    给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格. 设计一个算法来计算你所能获取的最大利润.你可以尽可能地完成更多的交易(多次买卖一支股票). 注意:你不能同时参与多笔交易(你必须在再次 ...

  3. SQL Server日志恢复还原数据

    通过日志还原,首先要注意的是: 1,在数据库更新和删除之前有一个完整的备份. 2,在更新和删除之后,做一个日志备份. 3,该日志只能用于还原数据库备份和日志备份时间之间的数据. 下面看整个数据库备份和 ...

  4. JAVA把InputStream 转 字节数组(byte[])

    import org.apache.commons.io.IOUtils; byte[] bytes = IOUtils.toByteArray(inputStream); 如果没有这个包 就加下依赖 ...

  5. 【LeetCode】108. Convert Sorted Array to Binary Search Tree 解题报告 (Java & Python)

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

  6. 【LeetCode】442. Find All Duplicates in an Array 解题报告(Python& C++)

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

  7. 【LeetCode】22. Generate Parentheses 括号生成

    作者: 负雪明烛 id: fuxuemingzhu 个人博客:http://fuxuemingzhu.cn/ 个人公众号:负雪明烛 本文关键词:括号, 括号生成,题解,leetcode, 力扣,Pyt ...

  8. 【LeetCode】676. Implement Magic Dictionary 解题报告(Python & C++)

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

  9. 基于CA认证(结合文档在线预览)的电子签章解决方案

    分享一个基于CA认证(结合文档在线预览)的电子签章实现思路,恰巧是最近项目中遇到的,欢迎大家一起讨论. 一. 项目背景 在公司业务系统中,按照传统的签章方式,存在以下痛点: 1.成本高,体现在纸质合同 ...

  10. Linux的基本目录结构