1. 为什么需要源码生成器
在.NET生态中,源码生成器(Source Generator)已经成为现代开发流程中不可或缺的工具。它能够在编译期间自动生成C#代码,与传统的代码生成工具相比,源码生成器直接集成在编译管道中,无需额外构建步骤,也不会产生中间文件。
我最近在一个大型微服务项目中深刻体会到源码生成器的价值。项目中需要为200多个DTO类生成序列化代码,手动编写不仅耗时,而且容易出错。通过源码生成器,我们实现了:
- 编译时自动生成基于System.Text.Json的高性能序列化代码
- 完全类型安全的代码生成
- 与项目代码的无缝集成
- 零运行时开销
partial类(部分类)是源码生成器的完美搭档。它允许我们将生成的代码与手写代码分离,同时保持逻辑上的完整性。这种范式特别适合:
- ORM实体类的扩展
- API客户端代理
- 协议缓冲区序列化
- 各种样板代码的自动生成
2. 创建源码生成器项目
2.1 项目结构规划
一个标准的源码生成器解决方案通常包含三个项目:
- 生成器项目:包含实际的生成逻辑
- 运行时库项目:包含生成代码依赖的类型
- 测试项目:验证生成器行为
我推荐以下项目结构:
code复制/SolutionFolder
│
├── /src
│ ├── MyGenerator (生成器项目)
│ └── MyGenerator.Runtime (运行时库)
│
└── /test
├── MyGenerator.Tests (单元测试)
└── MyGenerator.IntegrationTests (集成测试)
2.2 生成器项目配置
生成器项目需要特殊的项目配置:
xml复制<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>9.0</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
</ItemGroup>
</Project>
关键点说明:
netstandard2.0确保最广泛的兼容性EnforceExtendedAnalyzerRules启用完整的分析器功能PrivateAssets="all"确保依赖不会传递到消费项目
3. 实现生成器逻辑
3.1 基本生成器结构
每个源码生成器都需要实现ISourceGenerator接口:
csharp复制[Generator]
public class MySourceGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// 注册语法接收器或初始化工作
}
public void Execute(GeneratorExecutionContext context)
{
// 主要生成逻辑
}
}
3.2 使用SyntaxReceiver收集信息
高效的做法是使用ISyntaxContextReceiver收集编译过程中的语法信息:
csharp复制class MySyntaxReceiver : ISyntaxContextReceiver
{
public List<INamedTypeSymbol> TargetClasses { get; } = new();
public void OnVisitSyntaxNode(GeneratorSyntaxContext context)
{
if (context.Node is ClassDeclarationSyntax classDecl &&
classDecl.Modifiers.Any(SyntaxKind.PartialKeyword))
{
var symbol = context.SemanticModel.GetDeclaredSymbol(classDecl);
if (symbol != null)
{
TargetClasses.Add(symbol);
}
}
}
}
3.3 生成partial类扩展
以下是一个典型的生成逻辑示例:
csharp复制void GeneratePartialExtensions(GeneratorExecutionContext context)
{
var receiver = context.SyntaxContextReceiver as MySyntaxReceiver;
foreach (var classSymbol in receiver.TargetClasses)
{
string namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
string className = classSymbol.Name;
string source = $$"""
// <auto-generated/>
namespace {{namespaceName}};
public partial class {{className}}
{
public string GeneratedProperty => "Generated at {{DateTime.UtcNow:O}}";
public void GeneratedMethod()
{
Console.WriteLine("This is generated method");
}
}
""";
context.AddSource($"{className}.generated.cs", source);
}
}
4. NuGet打包最佳实践
4.1 多目标打包策略
为了最大化兼容性,我推荐采用多目标打包:
xml复制<PropertyGroup>
<PackageId>MyAwesome.Generator</PackageId>
<Version>1.0.0</Version>
<Description>An awesome source generator</Description>
<PackageTags>source-generator;code-generation;productivity</PackageTags>
</PropertyGroup>
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true"
PackagePath="analyzers/dotnet/cs" Visible="false" />
<None Include="$(OutputPath)\MyGenerator.Runtime.dll" Pack="true"
PackagePath="lib/netstandard2.0" Visible="false" />
</ItemGroup>
4.2 依赖管理
正确处理依赖关系至关重要:
xml复制<ItemGroup>
<PackageReference Include="MyGenerator.Runtime" Version="1.0.0" PrivateAssets="all" />
<ProjectReference Include="..\MyGenerator.Runtime\MyGenerator.Runtime.csproj"
PrivateAssets="all" ReferenceOutputAssembly="false" />
</ItemGroup>
4.3 版本控制策略
我建议采用语义化版本控制:
- 主版本号:破坏性变更
- 次版本号:新增功能(向后兼容)
- 修订号:Bug修复
同时使用<PackageVersion>属性确保所有相关包版本一致。
5. 调试与测试技巧
5.1 调试源码生成器
调试源码生成器有几种有效方法:
- 使用Debugger.Launch()
csharp复制public void Initialize(GeneratorInitializationContext context)
{
#if DEBUG
if (!Debugger.IsAttached)
{
Debugger.Launch();
}
#endif
}
- 日志输出
csharp复制context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(
"SG0001",
"Debug Info",
$"Processing {classSymbol.Name}",
"Debug",
DiagnosticSeverity.Info,
true),
Location.None));
5.2 单元测试策略
测试源码生成器需要特殊方法:
csharp复制[Test]
public void TestGeneratorOutput()
{
// 准备测试代码
string testCode = """
public partial class MyClass {}
""";
// 创建编译
var compilation = CSharpCompilation.Create("TestAssembly")
.AddSyntaxTrees(CSharpSyntaxTree.ParseText(testCode))
.AddReferences(MetadataReference.CreateFromFile(typeof(object).Assembly.Location));
// 创建生成器
var generator = new MySourceGenerator();
CSharpGeneratorDriver.Create(generator)
.RunGeneratorsAndUpdateCompilation(compilation,
out var outputCompilation,
out var diagnostics);
// 验证输出
Assert.IsFalse(diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error));
Assert.AreEqual(2, outputCompilation.SyntaxTrees.Count());
}
6. 性能优化技巧
经过多个项目实践,我总结了以下性能优化要点:
- 增量生成:使用
IncrementalGeneratorAPI替代ISourceGenerator
csharp复制[Generator]
public class MyIncrementalGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (s, _) => s is ClassDeclarationSyntax c && c.Modifiers.Any(SyntaxKind.PartialKeyword),
transform: (ctx, _) => (ClassDeclarationSyntax)ctx.Node);
context.RegisterSourceOutput(classDeclarations,
(spc, source) => GeneratePartialExtension(spc, source));
}
}
- 缓存策略:缓存语法树分析结果
- 并行处理:对独立任务使用
Parallel.ForEach - 减少IO:避免在生成器中执行文件操作
7. 实际应用案例
7.1 DTO自动映射
在微服务架构中,我们经常需要在不同层之间转换DTO。以下是一个自动生成映射代码的示例:
csharp复制string GenerateMapper(INamedTypeSymbol source, INamedTypeSymbol target)
{
var sourceProps = source.GetMembers().OfType<IPropertySymbol>();
var targetProps = target.GetMembers().OfType<IPropertySymbol>();
var mappings = sourceProps.Join(targetProps,
s => s.Name, t => t.Name,
(s, t) => new { Source = s, Target = t });
var code = new StringBuilder();
foreach (var map in mappings)
{
code.AppendLine($"{map.Target.Name} = source.{map.Source.Name},");
}
return $$"""
public static {{target.Name}} To{{target.Name}}(this {{source.Name}} source)
{
return new {{target.Name}}
{
{{code}}
};
}
""";
}
7.2 API客户端代理
为Web API自动生成强类型客户端:
csharp复制void GenerateApiClient(GeneratorExecutionContext context, INamedTypeSymbol controllerSymbol)
{
var methods = controllerSymbol.GetMembers()
.OfType<IMethodSymbol>()
.Where(m => m.DeclaredAccessibility == Accessibility.Public);
var clientCode = new StringBuilder();
foreach (var method in methods)
{
var httpMethod = GetHttpMethodAttribute(method);
clientCode.AppendLine(GenerateClientMethod(method, httpMethod));
}
context.AddSource($"{controllerSymbol.Name}Client.g.cs", $$"""
public partial class {{controllerSymbol.Name}}Client
{
private readonly HttpClient _client;
public {{controllerSymbol.Name}}Client(HttpClient client)
{
_client = client;
}
{{clientCode}}
}
""");
}
8. 常见问题与解决方案
8.1 生成代码不可见
问题:生成的代码在IDE中不可见或不起作用。
解决方案:
- 确保项目文件包含:
xml复制<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
- 检查生成器是否被正确引用:
xml复制<ItemGroup>
<ProjectReference Include="..\MyGenerator\MyGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
8.2 版本冲突
问题:不同版本的生成器或依赖项导致冲突。
解决方案:
- 使用强名称签名
- 严格管理依赖版本
- 考虑使用
InternalsVisibleTo共享类型
8.3 性能问题
问题:生成器显著增加编译时间。
优化建议:
- 使用
IncrementalGeneratorAPI - 减少语法树遍历
- 缓存中间结果
- 并行化独立任务
9. 进阶技巧
9.1 跨项目生成
有时需要在生成器中引用其他项目的类型。我的经验是:
- 创建共享的"契约"项目,包含接口和抽象类
- 在生成器中通过
Compilation.GetTypeByMetadataName获取类型符号 - 使用
[assembly: InternalsVisibleTo]共享必要类型
9.2 动态模板引擎
对于复杂生成逻辑,可以集成模板引擎:
csharp复制string GenerateWithTemplate(INamedTypeSymbol symbol)
{
var template = """
public partial class {{ClassName}}
{
{{#Each Properties}}
public {{Type}} {{Name}} { get; set; }
{{/Each}}
}
""";
var engine = new HandlebarsDotNet.Handlebars();
var compiled = engine.Compile(template);
return compiled(new {
ClassName = symbol.Name,
Properties = symbol.GetMembers()
.OfType<IPropertySymbol>()
.Select(p => new {
Name = p.Name,
Type = p.Type.ToDisplayString()
})
});
}
9.3 诊断与分析
结合分析器提供实时反馈:
csharp复制context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(
"SG1001",
"Partial class recommended",
"Class {0} should be partial to work with source generator",
"Design",
DiagnosticSeverity.Warning,
true),
Location.Create(classSyntax.SyntaxTree, classSyntax.Identifier.Span),
classSyntax.Identifier.Text));
10. 生态整合
10.1 与Roslyn分析器集成
源码生成器可以与Roslyn分析器配合使用,提供完整的代码分析和生成解决方案:
csharp复制// 分析器
public class MyAnalyzer : DiagnosticAnalyzer
{
public override void Initialize(AnalysisContext context)
{
context.RegisterSymbolAction(AnalyzeClass, SymbolKind.NamedType);
}
void AnalyzeClass(SymbolAnalysisContext context)
{
if (context.Symbol is INamedTypeSymbol typeSymbol &&
ShouldGenerateCodeFor(typeSymbol) &&
!typeSymbol.IsPartial())
{
context.ReportDiagnostic(CreateDiagnostic(typeSymbol));
}
}
}
// 生成器
public class MyGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classes = context.SyntaxProvider
.CreateSyntaxProvider(FindPartialClasses, GetClassSymbol)
.Where(ShouldGenerateCodeFor);
context.RegisterSourceOutput(classes, GenerateCode);
}
}
10.2 与构建系统集成
在CI/CD管道中优化生成器使用:
- 缓存生成结果:利用
[GeneratedCode]属性避免重复生成 - 增量编译:确保生成器支持增量生成
- 并行执行:对于大型解决方案,考虑并行运行多个生成器
10.3 IDE体验优化
提升开发体验的技巧:
- 快速信息提示:为生成的代码添加XML文档注释
csharp复制context.AddSource($"{className}.g.cs", $$"""
/// <summary>
/// Auto-generated extension for {{className}}
/// </summary>
public partial class {{className}}
{
/// <summary>
/// Generated method - {{DateTime.UtcNow:yyyy-MM-dd}}
/// </summary>
public void GeneratedMethod() { }
}
""");
- 错误恢复:处理生成错误时提供有意义的错误信息
- 进度反馈:长时间运行的生成器应该报告进度
通过以上方法,我们可以构建出既强大又用户友好的源码生成器,显著提升.NET开发效率和质量。在实际项目中,我建议从小规模开始,逐步扩展生成器的功能,同时密切关注性能和稳定性。
