1. SourceGenerator与partial范式的深度解析
在C# 9.0时代,SourceGenerator作为编译时代码生成利器彻底改变了传统开发模式。不同于运行时反射或AOP方案,它能在编译阶段直接分析代码结构并生成新的C#源文件,这种机制特别适合与partial类型配合使用。我最近在重构一个大型项目时,发现这种组合能解决许多架构难题。
partial关键字自C# 2.0就存在,但直到与SourceGenerator结合才真正释放其潜力。想象一下:你有一个业务实体类需要自动生成DTO、验证器等配套代码。传统做法要么污染主类,要么需要复杂构建流程。而通过partial分割,主类保持简洁,所有衍生代码通过SourceGenerator自动生成,最终编译时合并为完整类。
重要提示:SourceGenerator执行在编译管线早期,此时尚未生成IL代码。这意味着它只能基于语法树分析,无法获取运行时信息。
1.1 典型应用场景分析
在电商订单系统中,我实践了这种模式。核心订单类仅保留业务属性定义:
csharp复制// 手工编写部分
public partial class Order
{
public int Id { get; set; }
public DateTime CreateTime { get; set; }
// 其他核心字段...
}
通过SourceGenerator自动生成:
csharp复制// 自动生成部分
public partial class Order
{
public string ToJson() => JsonSerializer.Serialize(this);
public static Order FromJson(string json) => JsonSerializer.Deserialize<Order>(json);
// 自动生成的验证逻辑
public IEnumerable<string> Validate()
{
if(Id <= 0) yield return "ID必须大于0";
// 其他验证规则...
}
}
这种分离使得:
- 核心类定义保持稳定
- 派生功能可随需求动态调整
- 避免手动同步带来的错误
2. SourceGenerator实现细节剖析
2.1 生成器基础架构
创建一个完整的SourceGenerator需要实现以下接口:
csharp复制[Generator]
public class CustomGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// 注册语法树回调
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxReceiver is not SyntaxReceiver receiver)
return;
// 核心生成逻辑
var sourceCode = BuildSource(receiver.CandidateClasses);
context.AddSource("GeneratedCode.cs", SourceText.From(sourceCode, Encoding.UTF8));
}
}
关键点在于SyntaxReceiver的设计,它负责收集满足条件的partial类:
csharp复制class SyntaxReceiver : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> CandidateClasses { get; } = new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax classDecl &&
classDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
{
CandidateClasses.Add(classDecl);
}
}
}
2.2 元数据处理技巧
在实际项目中,我总结出几个提升生成代码质量的经验:
- 命名空间自动匹配:通过语法树获取目标类的命名空间,确保生成代码在相同空间下
csharp复制var namespace = classDecl.FirstAncestorOrSelf<NamespaceDeclarationSyntax>()?.Name.ToString();
- 类型依赖分析:使用SemanticModel解析类型符号,正确处理泛型等复杂场景
csharp复制var model = context.Compilation.GetSemanticModel(syntaxTree);
var typeSymbol = model.GetDeclaredSymbol(classDecl);
- 代码格式化:使用NormalizeWhitespace保证生成代码风格一致
csharp复制var formattedNode = syntaxNode.NormalizeWhitespace();
3. 测试策略与实战方案
3.1 单元测试的特殊挑战
测试SourceGenerator不同于常规代码测试,主要难点在于:
- 需要模拟编译管线环境
- 无法直接调试生成器逻辑
- 多阶段验证(语法分析、代码生成、编译结果)
我的解决方案是采用Microsoft.CodeAnalysis.Testing包:
csharp复制[Test]
public async Task Should_Generate_JsonMethods()
{
// 准备测试代码
const string source = @"
public partial class Order { public int Id { get; set; } }
";
// 预期生成的代码
const string generated = @"
public partial class Order { /* 预期的方法实现 */ }
";
var test = new CSharpSourceGeneratorTest<CustomGenerator, XUnitVerifier>
{
TestState = { Sources = { source } },
GeneratedSources =
{
(typeof(CustomGenerator), "GeneratedCode.cs", generated)
}
};
await test.RunAsync();
}
3.2 集成测试方案
对于复杂生成器,我建立了三层验证体系:
- 语法树测试:验证是否能正确识别目标类
csharp复制var syntaxTree = CSharpSyntaxTree.ParseText(source);
var compilation = CSharpCompilation.Create("Test")
.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location))
.AddSyntaxTrees(syntaxTree);
var generator = new CustomGenerator();
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
driver = driver.RunGenerators(compilation);
var result = driver.GetRunResult();
Assert.That(result.GeneratedTrees, Is.Not.Empty);
- 编译结果测试:确保生成的代码可编译
csharp复制var diagnostics = result.Compilation.GetDiagnostics();
Assert.That(diagnostics.Where(d => d.Severity == DiagnosticSeverity.Error), Is.Empty);
- 运行时行为测试:通过反射验证生成方法
csharp复制dynamic instance = Activator.CreateInstance("TestAssembly", "Order");
instance.Id = 123;
string json = instance.ToJson();
Assert.That(json, Contains.Substring("123"));
4. 性能优化与疑难排解
4.1 增量生成策略
在大规模项目中,我发现原始实现会导致编译时间线性增长。通过实现增量生成,性能提升显著:
csharp复制// 在Initialize中注册增量生成管道
context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
context.RegisterPostInitializationOutput(ctx =>
ctx.AddSource("Attributes.cs", SourceText.From(AttributesCode, Encoding.UTF8)));
context.RegisterImplementationSourceOutput(
context.SyntaxProvider.CreateSyntaxProvider(
predicate: static (node, _) => IsTargetNode(node),
transform: static (ctx, _) => GetGenerationData(ctx)),
static (ctx, data) => ExecuteGeneration(ctx, data));
关键优化点:
- 使用SyntaxProvider替代全量语法树扫描
- 缓存中间分析结果
- 分层生成策略(基础属性→衍生方法)
4.2 常见问题排查指南
根据实战经验整理的高频问题:
| 问题现象 | 排查方向 | 解决方案 |
|---|---|---|
| 生成代码未生效 | 1. 检查生成器是否注册 2. 验证目标类是否为partial |
在.csproj中添加<CompilerVisibleProperty> |
| 编译错误CS0282 | 生成的部分类未正确合并 | 确保所有partial类在相同命名空间和程序集 |
| 性能低下 | 1. 语法树遍历效率 2. 重复生成相同代码 |
实现ISyntaxContextReceiver替代ISyntaxReceiver |
| 无法调试 | 调试器未附加到编译进程 | 在生成器中添加Debugger.Launch() |
经验之谈:当生成代码出现问题时,首先检查obj目录下的generated文件夹,这里保存着实际生成的中间文件,比日志更直观。
5. 高级应用模式探索
5.1 多文件协同生成
在复杂场景下,单个类可能需要拆分成多个生成文件。我的做法是:
csharp复制// 主生成文件
context.AddSource($"{className}.g.cs", mainSource);
// 扩展功能文件
context.AddSource($"{className}.extensions.g.cs", extensionSource);
// 设计时元数据文件
context.AddSource($"{className}.metadata.g.cs", metadataSource);
文件命名约定:
.g.cs:核心生成内容.extensions.g.cs:辅助方法.designer.cs:设计时支持
5.2 跨项目生成策略
当需要在多个项目间共享生成逻辑时,我采用这种架构:
code复制SharedGenerators/
├── build/
│ ├── SharedGenerators.props
│ └── SharedGenerators.targets
└── src/
├── GeneratorCore.csproj
└── Generators/
├── DTOGenerator.cs
└── ValidatorGenerator.cs
关键配置项:
xml复制<!-- SharedGenerators.props -->
<ItemGroup>
<ProjectReference Include="..\src\GeneratorCore.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"/>
</ItemGroup>
这种结构允许:
- 集中管理生成器代码
- 多项目共享同一套生成规则
- 独立版本控制
在最近参与的微服务项目中,这种架构使得20+服务能保持一致的DTO生成策略,同时允许单个服务按需扩展生成规则。
