1. SourceGenerator与partial范式的技术背景
在C# 9.0时代,微软引入了Source Generator这一革命性功能,它允许开发者在编译期间动态生成代码。与传统代码生成工具不同,Source Generator直接集成在编译管道中,能够实时分析项目代码并生成新的源文件。这种机制特别适合与partial类结合使用,形成一种我称之为"partial范式"的开发模式。
partial关键字在C#中早已存在,但直到Source Generator出现才真正展现出其威力。通过将类声明为partial,我们可以把自动生成的代码和手写代码分离到不同文件中。这种分离不是简单的物理分割,而是形成了清晰的逻辑边界:生成器负责机械化的模板代码,开发者专注于业务逻辑实现。
我在实际项目中发现,这种范式特别适合解决以下三类问题:
- 重复性样板代码(如DTO属性、INotifyPropertyChanged实现)
- 需要根据元数据动态生成的代码(如gRPC服务端桩代码)
- 需要深度集成编译器信息的场景(如AOP拦截器)
2. 构建Source Generator项目的最佳实践
2.1 项目结构与依赖配置
创建一个标准的Source Generator项目需要特别注意依赖关系。我推荐使用.NET 6+ SDK风格的项目文件,并确保包含这些关键包引用:
xml复制<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" PrivateAssets="all" />
</ItemGroup>
提示:一定要设置PrivateAssets="all",这可以防止你的生成器依赖被传递到消费项目中。
项目结构通常这样组织:
code复制/MyGenerator
├── MyGenerator.csproj
├── MyGenerator.cs # 主生成器实现
├── Templates/ # 代码模板资源
└── TestProject/ # 测试项目(详见第4章)
2.2 实现生成器的核心逻辑
一个基础的生成器需要继承自ISourceGenerator接口。下面是我在多个项目中总结出的模板代码:
csharp复制[Generator]
public class MyGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// 注册语法接收器或编译动作
context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxReceiver is not MySyntaxReceiver receiver)
return;
// 获取编译对象
var compilation = context.Compilation;
// 处理收集到的语法节点
foreach (var classDecl in receiver.CandidateClasses)
{
var model = compilation.GetSemanticModel(classDecl.SyntaxTree);
var typeSymbol = model.GetDeclaredSymbol(classDecl);
// 生成代码逻辑
string source = GeneratePartialClass(typeSymbol);
context.AddSource($"{typeSymbol.Name}.g.cs", SourceText.From(source, Encoding.UTF8));
}
}
private string GeneratePartialClass(INamedTypeSymbol typeSymbol)
{
// 实际生成代码的逻辑
return $$"""
// <auto-generated/>
namespace {{typeSymbol.ContainingNamespace}};
public partial class {{typeSymbol.Name}}
{
public void GeneratedMethod()
{
Console.WriteLine("This is generated code!");
}
}
""";
}
}
2.3 处理partial类的关键技巧
在实际项目中,我发现这些技巧能显著提高生成器质量:
- 命名空间处理:总是从符号获取命名空间,而不是硬编码
- 类型参数传递:正确处理泛型类的类型参数传递
- 注释生成:自动生成文件头注释并标记为auto-generated
- 增量生成:对于大型项目,实现增量生成策略
一个常见的陷阱是忽略了对已有partial部分的检查。好的做法是在生成代码前检查目标类是否已经包含某些成员:
csharp复制bool hasMethod = typeSymbol.GetMembers()
.Any(m => m.Name == "MyMethod" && m.Kind == SymbolKind.Method);
3. partial范式的典型应用场景
3.1 DTO自动生成
在微服务架构中,我们经常需要为API契约生成数据传输对象。使用Source Generator可以基于接口定义自动生成完整的DTO类:
csharp复制// 手写部分
public partial record UserDto
{
public interface IUser
{
int Id { get; }
string Name { get; }
}
}
// 生成部分
public partial record UserDto : IUser
{
public int Id { get; init; }
public string Name { get; init; }
// 自动生成的Equals/GetHashCode等
}
3.2 装饰器模式实现
通过partial类可以优雅地实现装饰器模式:
csharp复制// 手写部分
public partial interface IService
{
void DoWork();
}
public partial class Service
{
private void DoWorkCore() { /* 核心实现 */ }
}
// 生成部分
public partial class Service : IService
{
public void DoWork()
{
Console.WriteLine("Log: Method entry");
try {
DoWorkCore();
}
finally {
Console.WriteLine("Log: Method exit");
}
}
}
3.3 AOP拦截器
结合特性(Attribute)可以实现声明式的AOP:
csharp复制// 手写部分
[Logging]
public partial class OrderProcessor
{
private void ProcessOrderCore(Order order) { /* ... */ }
}
// 生成部分
public partial class OrderProcessor
{
public void ProcessOrder(Order order)
{
var stopwatch = Stopwatch.StartNew();
try {
ProcessOrderCore(order);
}
finally {
Console.WriteLine($"Elapsed: {stopwatch.ElapsedMilliseconds}ms");
}
}
}
4. Source Generator的测试策略
4.1 单元测试框架选择
测试Source Generator有其特殊性,我推荐使用这些工具组合:
- Microsoft.CodeAnalysis.CSharp.Testing 用于编译测试
- Xunit/NUnit 作为测试框架
- Verify 用于快照测试
测试项目需要特殊配置:
xml复制<ItemGroup>
<ProjectReference Include="..\MyGenerator\MyGenerator.csproj"
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
4.2 测试生成器的基础设施
我通常创建一个测试基类来处理重复的编译逻辑:
csharp复制public abstract class GeneratorTestBase
{
protected static (Compilation outputCompilation, ImmutableArray<Diagnostic> diagnostics)
RunGenerator(string source, params MetadataReference[] additionalReferences)
{
var compilation = CSharpCompilation.Create("TestAssembly",
new[] { CSharpSyntaxTree.ParseText(source) },
GetDefaultReferences(),
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var generator = new MyGenerator();
CSharpGeneratorDriver.Create(generator)
.RunGeneratorsAndUpdateCompilation(compilation,
out var outputCompilation,
out var diagnostics);
return (outputCompilation, diagnostics);
}
private static IEnumerable<MetadataReference> GetDefaultReferences()
{
yield return MetadataReference.CreateFromFile(typeof(object).Assembly.Location);
// 添加其他必要引用...
}
}
4.3 验证生成结果的三种方法
- 语法树验证:检查生成的语法树结构
csharp复制[Fact]
public void ShouldGeneratePartialClass()
{
var source = "public partial class MyClass { }";
var (_, diagnostics) = RunGenerator(source);
Assert.Empty(diagnostics);
// 进一步检查生成的语法树...
}
- 编译结果验证:确保生成的代码可编译
csharp复制[Fact]
public void GeneratedCodeShouldCompile()
{
var source = "public partial class MyClass { }";
var (outputCompilation, _) = RunGenerator(source);
using var peStream = new MemoryStream();
var emitResult = outputCompilation.Emit(peStream);
Assert.True(emitResult.Success);
}
- 功能验证:通过反射执行生成代码
csharp复制[Fact]
public void GeneratedMethodShouldWork()
{
var source = "public partial class MyClass { }";
var (outputCompilation, _) = RunGenerator(source);
using var ms = new MemoryStream();
outputCompilation.Emit(ms);
var assembly = Assembly.Load(ms.ToArray());
var type = assembly.GetType("MyClass");
var instance = Activator.CreateInstance(type);
type.GetMethod("GeneratedMethod").Invoke(instance, null);
// 验证输出...
}
4.4 集成测试的实用技巧
在实际项目中,我发现这些测试策略特别有用:
- 快照测试:使用Verify库对比生成的代码与预期模板
- 增量测试:验证对同一输入多次运行生成器是否幂等
- 性能测试:确保生成器不会显著拖慢编译速度
- 错误案例测试:故意提供错误输入验证错误处理
一个典型的快照测试示例:
csharp复制[Fact]
public async Task GeneratedCodeShouldMatchSnapshot()
{
var source = "public partial class MyClass { }";
var (outputCompilation, _) = RunGenerator(source);
var generatedTrees = outputCompilation.SyntaxTrees
.Where(t => t.FilePath.EndsWith(".g.cs"));
foreach (var tree in generatedTrees)
{
await Verifier.Verify(tree.GetText().ToString());
}
}
5. 生产环境中的经验教训
5.1 性能优化要点
在大型项目中,Source Generator可能成为编译瓶颈。通过以下优化,我曾将生成时间从12秒降至800毫秒:
- 增量生成:只处理变更的文件
- 缓存机制:缓存语法树分析结果
- 并行处理:对独立单元使用并行生成
- 延迟生成:非关键代码延迟到后期生成
实现增量生成的关键代码:
csharp复制public class MySyntaxReceiver : ISyntaxReceiver
{
private readonly HashSet<ClassDeclarationSyntax> _classes = new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax classDecl &&
classDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
{
_classes.Add(classDecl);
}
}
public IEnumerable<ClassDeclarationSyntax> CandidateClasses => _classes;
}
5.2 常见问题排查
根据我的踩坑经验,这些问题最常出现:
- 找不到partial类:检查是否正确定义了部分类
- 生成代码不更新:清理obj/bin目录
- 版本冲突:确保所有项目使用相同的Roslyn版本
- IDE缓存问题:重启VS或运行
dotnet build-server shutdown
一个实用的诊断方法是输出生成日志:
csharp复制context.AnalyzerConfigOptions.GlobalOptions.TryGetValue(
"build_property.GeneratorDebug", out var debug);
if (debug == "true")
{
File.WriteAllText(
Path.Combine(Environment.GetFolderPath(
Environment.SpecialFolder.Desktop),
"generator.log"),
diagnosticInfo);
}
5.3 团队协作建议
在团队中推广Source Generator时,这些实践很有帮助:
- 文档规范:详细记录每个生成器的职责和约定
- 代码审查:特别关注生成器与手写代码的边界
- 版本控制:将生成的代码纳入版本控制(争议性做法)
- IDE配置:统一团队的生成器开发环境配置
我发现在项目中添加一个GeneratedCode.md文件很有用,内容包含:
- 生成器列表及其功能
- 代码生成触发条件
- 自定义扩展点说明
- 常见问题解决方法
6. 未来演进方向
虽然Source Generator已经很强大,但根据我的使用经验,这些方向值得关注:
- 更好的IDE集成:实时预览生成代码
- 更智能的增量生成:基于语义而非语法的变更检测
- 跨语言生成:支持生成非C#代码(如TypeScript)
- 生成器组合:多个生成器协同工作
一个有趣的实验是让生成器消费其他生成器的输出:
csharp复制// 在生成器项目中
[Generator]
public class SecondGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var provider = context.SyntaxProvider
.CreateSyntaxProvider(/* ... */)
.Where(static node => node is not null);
context.RegisterSourceOutput(provider, (ctx, node) =>
{
// 可以在这里消费其他生成器的输出
});
}
}
在实际项目中,我发现partial范式特别适合中等复杂度的代码生成需求。对于极其简单的场景,传统T4模板可能更轻量;对于高度复杂的场景,可能需要考虑完整的DSL解决方案。但在这个中间地带,Source Generator与partial类的组合提供了绝佳的平衡点——既保持了编译时类型安全,又能显著减少样板代码。
