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

来源:这里教程网 时间:2026-02-21 17:40:27 作者:

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 中):

<ItemGroup>
  <ProjectReference Include="..\MyGenerator\MyGenerator.csproj" 
                    OutputItemType="Analyzer" 
                    ReferenceOutputAssembly="false" />
</ItemGroup>

为什么 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 class
partial struct
partial record
命名空间、类名、泛型参数数量与用户代码完全一致(大小写敏感) 不能生成构造函数或已有成员的重复定义(如已有
public void Foo()
,就不能再生成同签名方法)
若需注入字段/方法,建议加唯一后缀(如
__autoLogHandler
)避免命名污染

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

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

最容易被忽略的是命名空间嵌套层级和 using 指令缺失——generator 不自动继承目标文件的

using
,所有类型引用必须写全名或手动拼
using
行。

相关推荐

热文推荐