c# .net 静态织入 代码生成 Source Generators 编译时反射

通过Source Generators可以实现在编译期间注入代码,以此来优化运行时反射的效率。
另外也可以实现公共代码逻辑的注入,如WPF常用组件的Property.Fody库的通知效果(fody使用的是其他的技术模式); automapper也可以使用此模式来实现;自动添加GRPC接口。
Source Generators 可以作为T4模式编译时的替代方案。

T4模板在运行时动态生成的方案可以参考T4模板引擎 参数调用

Source Generators 基本使用

本文通过获取程序入口对应的名称空间及类名,并将对应的类调整为分部类,然后创建无参构造函数来实现编译时注入。
其他场景下可以通过分析语法树进行织入

  1. 创建项目【ClassLibrary1】来存放代码生成接口并引用Microsoft.CodeAnalysis.CSharp 【必须创建 netstandard项目】
 [Generator]
    public class DemoSourceGenerator : ISourceGenerator
    {
        public void Execute(GeneratorExecutionContext context)
        {
            // Find the main method
            var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);

            // Build up the source code
            string source = $@"// <auto-generated/>
using System;

namespace {mainMethod.ContainingNamespace.ToDisplayString()}
{{
    public static partial class {mainMethod.ContainingType.Name}
    {{
        static partial void HelloFrom(string name) =>
            Console.WriteLine($""Generator says: Hi from '{{name}}'"");
    }}
}}
";
            var typeName = mainMethod.ContainingType.Name;

            // Add the source code to the compilation
            context.AddSource($"{typeName}.g.cs", source);
        }

        public void Initialize(GeneratorInitializationContext context)
        {
            // No initialization required for this one
        }
    }
  1. 创建控制台测试程序【ConsoleApp1】
partial class Program
    {
        static void Main(string[] args)
        {
            HelloFrom("Generated Code");
        }

        static partial void HelloFrom(string name);
    }
  1. 配置【ConsoleApp1】添加 OutputItemType="Analyzer" ReferenceOutputAssembly="false"
![](https://img2023.cnblogs.com/blog/944369/202304/944369-20230404183201456-1511027424.png)

  1. 运行得到结果

  2. 生成的代码

实现在程序启动时注入一段代码

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

namespace Common.Toolkit.LicenseAutoInject
{
    [Generator]
    public class LicenseCheckSourceGenerator : ISourceGenerator
    {

        public void Execute(GeneratorExecutionContext context)
        {
            var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);

            // Build up the source code
            string source = $@"// <auto-generated/>
using System;

namespace {mainMethod.ContainingNamespace.ToDisplayString()}
{{
    public static partial class {mainMethod.ContainingType.Name}
    {{
        static     {mainMethod.ContainingType.Name}(  )
        {{
            Console.WriteLine(""program start"");

        }}
    }}
}}
";

            context.AddSource("Program.Static.Main.cs", SourceText.From(source, Encoding.UTF8));
        }

        public void Initialize(GeneratorInitializationContext context)
        {
        }
    }
}

最终生成的代码如下

// <auto-generated/>
using System;

namespace Common.Toolkit.Auto.Test
{
    public static partial class Program
    {
        static     Program(  )
        {
            Console.WriteLine("init license");
        }
    }
}

发布Nuget

在生成器的项目文件中添加配置

  <!--确保生成输出最终位于NuGet包的analyzers/dotnet/cs文件夹中。 确保dLL不会出现在NuGet包的normal”文件夹中。-->
<ItemGroup>
	<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false"></None>
</ItemGroup>
<!--使用项目不会获得对源生成器dLL本身的明引用-->
<PropertyGroup>
	<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>

添加这两个配置项后其他项目直接添加nuget即可引入代码生成器

也可以使用baget配置私有nuget服务器上传nuget

注意

根据相关文章需要注意下列事项

  1. 调试
    在生成器的逻辑中添加Debugger.Launch()来启动调试器

  2. 生成的代码有时候查看不到
    建议在引用生成器的项目添加 true
    来在obj/Debug/$(TargetFramework)/generated目录生成源码

  1. 发送警告、错误给编译器
 DiagnosticDescriptor InvalidXmlWarning = new DiagnosticDescriptor(id: "MYXMLGEN001",
                                                                                              title: "Couldn't parse XML file",
                                                                                              messageFormat: "Couldn't parse XML file '{0}'.",
                                                                                              category: "MyXmlGenerator",
                                                                                              DiagnosticSeverity.Warning,
                                                                                              isEnabledByDefault: true);
            context.ReportDiagnostic(Diagnostic.Create(InvalidXmlWarning, Location.None, $"{typeName}.g.cs"));

Demo

  1. 关键对象
Microsoft.CodeAnalysis.SyntaxTree:包含单个文件里所有语法节点的语法树
Microsoft.CodeAnalysis.Compilation: 包含整个编译项目的编译信息

GeneratorExecutionContext.Compilation 即整个项目的编译信息;
GeneratorExecutionContext.Compilation.SyntaxTrees 包含整个项目正在参与编译的所有非生成器生成的代码的语法树。


var semanticModel = compilation.GetSemanticModel(syntaxTree); 获取语义模型和语义符号;通过这个语义模型,你可以找到每一个语法节点所对应的语义符号到底是什么。
接下来的部分,你需要先拥有 Roslyn 语法分析的基本能力才能完成,因为要拿到一个语义符号,你需要先拿到其对应的语法节点(至少是第一个节点)。例如,拿到一个语法树(SyntaxTree)中的类型定义,可以用下面的方法:
// 遍历语法树中的所有节点,找到所有类型定义的节点。
var classDeclarationSyntaxes = from node in syntaxTree.GetRoot().DescendantNodes()
                               where node.IsKind(SyntaxKind.ClassDeclaration)
                               select (ClassDeclarationSyntax) node;
这样,针对这个语法树里面的每一个类型定义,我们都可以拿到其对应的语义了:
foreach (var classDeclarationSyntax in classDeclarationSyntaxes)
{
    if (semanticModel.GetDeclaredSymbol(classDeclarationSyntax) is { } classDeclarationSymbol)
    {
        // 在这里使用你的类型定义语义符号。
    }
}

// 获取类型的命名空间。
var namespace = classDeclarationSymbol.ContainingNamespace;

// 获得基类,获得接口。
var baseType = classDeclarationSymbol.BaseType;
var interfaces = classDeclarationSymbol.Interfaces;

// 获取类型的成员。
var members = classDeclarationSymbol.GetMembers();

// 获取成员的类型,然后忽略掉属性里面的方法。
foreach (var member in members)
{
    if (member is IMethodSymbol method && method.MethodKind is MethodKind.PropertyGet or MethodKind.PropertySet)
    {
        continue;
    }

    // 其他成员。
}


// 获得方法的形参数列表。
var parameters = method.Parameters;

// 获得方法的返回值类型。
var returnType = method.ReturnType;



尾注

使用source generators可能会导致修改问题后反复触发编译从而导致VS卡死,另由于VS热重载机制,后续source generators废弃,改用 incremental generators

语法可视化窗口 - 视图 -> 其他窗口 -> syntax visualizer

相关内容还有DiagnosticAnalyzer、CodeFixProvider

[参考]
源生成器
抽丝剥茧!Source Generators原理讲解
C#代码生成器打包
C# 强大的新特性 Source Generator
SourceGenerator入门指北
source-generators.cookbook
尝试 IIncrementalGenerator 进行增量 Source Generator 生成代码
使用 Roslyn 对 C# 代码进行语义分析

posted @ 2023-04-04 18:36  Hey,Coder!  阅读(461)  评论(0编辑  收藏  举报