c# .net 静态织入 代码生成 Source Generators 编译时反射
通过Source Generators可以实现在编译期间注入代码,以此来优化运行时反射的效率。
另外也可以实现公共代码逻辑的注入,如WPF常用组件的Property.Fody库的通知效果(fody使用的是其他的技术模式); automapper也可以使用此模式来实现;自动添加GRPC接口。
Source Generators 可以作为T4模式编译时的替代方案。
T4模板在运行时动态生成的方案可以参考T4模板引擎 参数调用
Source Generators 基本使用
本文通过获取程序入口对应的名称空间及类名,并将对应的类调整为分部类,然后创建无参构造函数来实现编译时注入。
其他场景下可以通过分析语法树进行织入
- 创建项目【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
}
}
- 创建控制台测试程序【ConsoleApp1】
partial class Program
{
static void Main(string[] args)
{
HelloFrom("Generated Code");
}
static partial void HelloFrom(string name);
}
- 配置【ConsoleApp1】添加 OutputItemType="Analyzer" ReferenceOutputAssembly="false"
![](https://img2023.cnblogs.com/blog/944369/202304/944369-20230404183201456-1511027424.png)
-
运行得到结果
-
生成的代码
实现在程序启动时注入一段代码
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
注意
根据相关文章需要注意下列事项
-
调试
在生成器的逻辑中添加Debugger.Launch()来启动调试器 -
生成的代码有时候查看不到
建议在引用生成器的项目添加true
来在obj/Debug/$(TargetFramework)/generated目录生成源码
- 发送警告、错误给编译器
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"));
- 关键对象
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# 代码进行语义分析
联系我:renhanlinbsl@163.com