C# 源代码生成器方法 C#如何创建自己的Source Generator

7次阅读

Source Generator 是 C# 9+ 的编译时代码生成机制,通过实现 IIncrementalGenerator 在 Roslyn 编译阶段注入 partial 类型,用于自动实现接口、生成 DTO、注入日志等,不修改源码、不支持运行时或已编译程序集。

C# 源代码生成器方法 C#如何创建自己的Source Generator

Source Generator 是什么,它能做什么

Source Generator 是 C# 9+ 提供的编译时代码生成机制,不是运行时反射或模板引擎。它在 Roslyn 编译器执行语法分析阶段介入,通过实现 IIncrementalGenerator 接口,在不修改原始源码的前提下,向编译流水线注入新的 C# 类型(如 partial class)。常见用途包括:自动实现 INotifyPropertyChanged、从 jsON Schema 生成 DTO、为标记了特定 Attribute 的类注入日志/验证逻辑。

它不能替代运行时代码生成(如 Reflection.Emit),也不处理已编译的程序集——只作用于当前项目正在编译的源码树。

创建一个最简 Source Generator 项目

需要两个分离的项目:一个是 generator 本身(类库),另一个是使用它的目标项目(通常为 .net 6+ SDK 风格的 console 或 Library)。

实操步骤:

  • 新建类库项目,目标框架设为 netstandard2.0(兼容性最好),添加 NuGet 包:microsoft.CodeAnalysis.CSharp(v4.0+)、Microsoft.CodeAnalysis.Analyzers(可选但推荐)
  • 添加对 System.Collections.Immutable 的引用(.NET 5+ 已内置,旧版需显式添加)
  • 实现 IIncrementalGenerator,重写 Initialize 方法;不要用传统 ISourceGenerator(已过时)
  • 在目标项目中,以 ProjectReference 方式引用 generator 项目,并设置 OutputItemType="Analyzer"ReferenceOutputAssembly="false"

关键配置示例(目标项目的 .csproj 中):

    

为什么 Initialize 方法里要用 IncrementalValueProvider

Roslyn 的增量编译模型要求 generator 显式声明依赖项(如语法树、语义模型、特定 Attribute),否则无法触发重生成。直接遍历 compilation.SyntaxTrees 会破坏增量性,导致每次编译都全量执行,拖慢构建速度。

正确做法是使用 context.SyntaxProvider + context.CompilationProvider 构建链式管道:

  • CreateSyntaxProvider 过滤出你关心的语法节点(比如所有带 [AutoLog] 的方法声明)
  • GetSemanticModelAsync 在后续阶段获取语义信息(如判断该方法是否在 partial class 中)
  • 最终用 RegisterSourceOutput 输出生成的代码字符串

错误写法(破坏增量性):

foreach (var tree in context.Compilation.SyntaxTrees) { ... }

正确起点示例:

context.RegisterSourceOutput(     context.SyntaxProvider         .CreateSyntaxProvider(             predicate: static (s, _) => s is MethodDeclarationSyntax m &&                  m.AttributeLists.Any(al => al.Attributes.Any(a => a.Name.ToString() == "AutoLog")),             transform: static (ctx, _) => ctx.Node as MethodDeclarationSyntax)         .Where(m => m != null),     Execute);

生成的代码必须是 partial 且命名空间/类名严格匹配

Source Generator 输出的代码会被 Roslyn 当作普通源文件参与编译,但它不能定义“全新”的非 partial 类——否则会和用户手写的类冲突,报 CS0101: The Namespace 'X' already contains a definition for 'Y'

所以必须遵守:

  • 生成的类型必须声明为 partial classpartial Structpartial record
  • 命名空间、类名、泛型参数数量与用户代码完全一致(大小写敏感)
  • 不能生成构造函数或已有成员的重复定义(如已有 public void Foo(),就不能再生成同签名方法)
  • 若需注入字段/方法,建议加唯一后缀(如 __autoLogHandler)避免命名污染

生成内容示例(字符串拼接,非 Roslyn SyntaxFactory):

var code = $$""" namespace {{@namespace}}; partial class {{className}} {     private void <>__AutoLog_{{methodName}}() { /* ... */ } } """;

最容易被忽略的是命名空间嵌套层级和 using 指令缺失——generator 不自动继承目标文件的 using,所有类型引用必须写全名或手动拼 using 行。

text=ZqhQzanResources