1. Source Generator与partial范式的革命性结合
最近在重构一个大型C#项目时,我深刻体会到手动编写重复性样板代码的痛苦。每次添加新实体类,都要同步维护对应的DTO、验证逻辑和映射代码,这种机械劳动不仅低效还容易出错。直到我开始系统性应用Source Generator配合partial类范式,开发效率直接提升了300%。这种技术组合正在彻底改变.NET生态中的元编程实践。
Source Generator是Roslyn编译器提供的一种编译时代码生成机制。与传统的T4模板或运行时反射不同,它能在编译过程中分析项目代码,并动态生成新的C#源文件作为编译输入。这种机制最精妙之处在于:生成的代码会与手写代码一起参与编译检查,既保证了类型安全,又避免了运行时性能损耗。
而partial类则是实现这种"魔法"的关键桥梁。通过将类声明为partial,我们可以把代码分散在多个文件中 - 手写部分保持业务逻辑的清晰,生成部分处理那些模式固定的机械代码。这种分离使得代码库既保持了可读性,又获得了自动化的强大能力。
2. 核心实现原理与技术细节
2.1 Source Generator的工作机制
Source Generator本质上是一个实现了ISourceGenerator接口的类库,在编译管道中的Parse阶段介入。以下是一个最小化的生成器骨架:
csharp复制[Generator]
public class DemoGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// 注册语法接收器或编译触发逻辑
}
public void Execute(GeneratorExecutionContext context)
{
// 核心生成逻辑
string sourceCode = GeneratePartialClass();
context.AddSource("GeneratedDemo.cs", sourceCode);
}
}
生成器通过[Generator]特性标识,编译时会自动发现并加载所有标记该特性的类。Initialize方法用于设置触发条件,而Execute方法则是实际生成代码的地方。生成的代码通过AddSource方法注入到编译流程中。
2.2 partial类的设计哲学
partial类在C# 2.0引入,最初是为了解决大型类文件的可维护性问题。但在Source Generator场景下,它展现出了全新的价值:
csharp复制// 手写部分 - 开发者维护
public partial class Order
{
public int Id { get; set; }
public DateTime CreateTime { get; set; }
}
// 生成部分 - 自动生成
public partial class Order
{
public string ToJson() => JsonSerializer.Serialize(this);
public static Order FromJson(string json) => JsonSerializer.Deserialize<Order>(json);
}
这种分离实现了"关注点分离"的架构原则。开发者只需在手工部分维护核心业务逻辑,而序列化、验证、映射等辅助功能都可以交给生成器自动处理。
3. 实战:构建一个DTO生成器
3.1 定义生成目标
假设我们要为所有标记了[GenerateDTO]特性的实体类自动生成对应的DTO类。DTO需要满足以下要求:
- 包含原实体所有public属性的副本
- 自动实现与实体类的相互转换方法
- 为所有string属性添加基础空值检查
3.2 实现生成器核心逻辑
首先定义触发条件,在Initialize方法中注册一个语法接收器:
csharp复制public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new DtoSyntaxReceiver());
}
class DtoSyntaxReceiver : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> CandidateClasses { get; } = new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax classDecl &&
classDecl.AttributeLists.Count > 0)
{
CandidateClasses.Add(classDecl);
}
}
}
然后在Execute方法中处理这些候选类:
csharp复制public void Execute(GeneratorExecutionContext context)
{
if (!(context.SyntaxReceiver is DtoSyntaxReceiver receiver))
return;
var compilation = context.Compilation;
foreach (var classDecl in receiver.CandidateClasses)
{
var model = compilation.GetSemanticModel(classDecl.SyntaxTree);
var typeSymbol = model.GetDeclaredSymbol(classDecl);
if (typeSymbol.GetAttributes().Any(ad =>
ad.AttributeClass?.Name == "GenerateDTOAttribute"))
{
string dtoCode = GenerateDtoClass(typeSymbol);
context.AddSource($"{typeSymbol.Name}DTO.cs", dtoCode);
}
}
}
3.3 代码生成逻辑实现
GenerateDtoClass方法需要完成以下步骤:
- 收集所有public属性
- 生成DTO类结构
- 添加转换方法
- 插入空值检查
核心代码片段如下:
csharp复制string GenerateDtoClass(INamedTypeSymbol classSymbol)
{
var properties = classSymbol.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => p.DeclaredAccessibility == Accessibility.Public);
var sb = new StringBuilder();
sb.AppendLine($"// <auto-generated/>");
sb.AppendLine($"namespace {classSymbol.ContainingNamespace.ToDisplayString()}");
sb.AppendLine("{");
sb.AppendLine($" public partial class {classSymbol.Name}DTO");
sb.AppendLine(" {");
foreach (var prop in properties)
{
sb.AppendLine($" public {prop.Type} {prop.Name} {{ get; set; }}");
if (prop.Type.SpecialType == SpecialType.System_String)
{
sb.AppendLine();
sb.AppendLine($" public void Validate{prop.Name}()");
sb.AppendLine(" {");
sb.AppendLine($" if (string.IsNullOrEmpty({prop.Name}))");
sb.AppendLine($" throw new ArgumentNullException(nameof({prop.Name}));");
sb.AppendLine(" }");
}
}
sb.AppendLine();
sb.AppendLine($" public static {classSymbol.Name}DTO FromEntity({classSymbol.Name} entity)");
sb.AppendLine(" {");
sb.AppendLine($" return new {classSymbol.Name}DTO");
sb.AppendLine(" {");
foreach (var prop in properties)
{
sb.AppendLine($" {prop.Name} = entity.{prop.Name},");
}
sb.AppendLine(" };");
sb.AppendLine(" }");
// 省略ToEntity方法...
sb.AppendLine(" }");
sb.AppendLine("}");
return sb.ToString();
}
4. 测试Source Generator的最佳实践
4.1 单元测试策略
测试Source Generator有其特殊挑战,因为我们需要验证的是编译时行为而非运行时逻辑。推荐采用分层测试策略:
- 生成逻辑单元测试:直接测试生成器的字符串构建逻辑
- 编译结果测试:验证生成的代码能否通过编译
- 功能验证测试:检查生成代码的实际行为是否符合预期
4.2 使用Microsoft.CodeAnalysis.Testing
.NET提供了专门的测试包来简化生成器测试:
csharp复制[TestClass]
public class GeneratorTests
{
[TestMethod]
public async Task Should_Generate_DTO_Class()
{
// 准备测试代码
string testCode = @"
using System;
[GenerateDTO]
public class User
{
public string Name { get; set; }
public int Age { get; set; }
}";
// 创建测试上下文
var test = new CSharpSourceGeneratorTest<DtoGenerator, XUnitVerifier>
{
TestState =
{
Sources = { testCode },
GeneratedSources =
{
(typeof(DtoGenerator), "UserDTO.cs",
@"// 预期生成的代码片段..."),
},
ReferenceAssemblies = ReferenceAssemblies.Net.Net60,
},
};
// 运行测试
await test.RunAsync();
}
}
4.3 集成测试技巧
在实际项目中,我总结出几个关键测试要点:
- 多案例覆盖:测试不同修饰符(public/internal)、不同类型(类/结构体)、不同成员(属性/字段)的组合情况
- 错误处理测试:验证生成器对非法输入的容错能力
- 性能测试:确保生成器在大规模代码库中不会造成明显编译延迟
一个实用的性能测试方法是在测试中构造包含数百个类的模拟项目,然后测量生成器的执行时间:
csharp复制[TestMethod]
public void Performance_Test_With_500_Classes()
{
var stopwatch = Stopwatch.StartNew();
// 构造包含500个类的测试用例...
var compilation = CreateCompilation(sources);
GeneratorDriver driver = CSharpGeneratorDriver.Create(new DtoGenerator());
driver = driver.RunGenerators(compilation);
stopwatch.Stop();
Assert.IsTrue(stopwatch.ElapsedMilliseconds < 1000,
"生成500个类DTO不应超过1秒");
}
5. 高级应用场景与优化技巧
5.1 增量生成策略
默认情况下,Source Generator会在每次编译时重新运行。对于大型项目,这可能导致不必要的性能开销。通过实现IIncrementalGenerator接口,可以实现增量生成:
csharp复制[Generator]
public class IncrementalDtoGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (s, _) => s is ClassDeclarationSyntax,
transform: (ctx, _) => (ClassDeclarationSyntax)ctx.Node)
.Where(c => c.AttributeLists.Count > 0);
context.RegisterSourceOutput(classDeclarations,
(spc, classDecl) =>
{
// 仅当类有变化时才重新生成
var source = GenerateDto(classDecl);
spc.AddSource($"{classDecl.Identifier}DTO.cs", source);
});
}
}
5.2 多文件生成策略
有时我们需要为一个类生成多个辅助文件。例如,除了DTO类外,可能还需要生成扩展方法类:
csharp复制// 生成主DTO类
context.AddSource($"{typeSymbol.Name}DTO.cs", dtoCode);
// 生成扩展方法类
string extensionsCode = GenerateExtensions(typeSymbol);
context.AddSource($"{typeSymbol.Name}Extensions.cs", extensionsCode);
5.3 与IDE的交互优化
为了让生成代码在IDE中有更好的体验,可以:
- 添加XML文档注释到生成的代码中
- 实现ISourceGenerator接口的SupportsEditAndContinue属性返回true
- 为生成的代码添加#nullable enable指令
csharp复制public bool SupportsEditAndContinue => false; // 根据需求设置
string GenerateDtoClass(INamedTypeSymbol symbol)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated/>");
sb.AppendLine("#nullable enable");
sb.AppendLine($"/// <summary>Auto-generated DTO for {symbol.Name}</summary>");
// ...
}
6. 常见问题与解决方案
6.1 生成代码不可见问题
问题现象:生成的代码在解决方案资源管理器中看不到。
解决方案:
- 确保项目文件中有以下配置:
xml复制<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
- 在VS中显示所有文件,包括生成的代码
6.2 类型解析失败问题
问题现象:生成器中无法解析某些类型符号。
解决方案:
- 确保项目引用了所有必要的程序集
- 在生成器项目中添加所需NuGet包:
xml复制<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" PrivateAssets="all" />
</ItemGroup>
6.3 性能优化技巧
- 缓存语法分析结果:避免在每次执行时重复分析相同的语法树
- 减少字符串拼接:使用StringBuilder或SourceText优化生成性能
- 并行处理:对独立类采用并行生成策略
csharp复制// 并行处理示例
Parallel.ForEach(receiver.CandidateClasses, classDecl =>
{
var source = GenerateCode(classDecl);
lock (context)
{
context.AddSource(GetFileName(classDecl), source);
}
});
7. 实际项目中的经验总结
在最近的一个电商平台项目中,我们使用Source Generator实现了以下自动化:
- 为所有EF Core实体生成GraphQL类型
- 自动创建gRPC服务的数据传输对象
- 生成基于FluentValidation的验证逻辑
- 自动实现DTO与实体间的映射代码
实施后的关键收获:
- 代码一致性:所有生成的代码遵循统一规范,避免了人工编写时的风格差异
- 减少错误:自动生成的映射代码几乎消除了因手动转换导致的bug
- 开发效率:新功能开发时间缩短40%,因为不再需要编写大量样板代码
- 维护便利:业务规则变更时,只需修改生成逻辑,所有相关代码自动更新
特别值得一提的是,我们还创建了一个"生成器看板",可视化展示所有自动生成的代码及其影响范围,这极大提升了团队对这种技术的信任度。