1. SourceGenerator与partial范式解析
在C# 9.0引入的Source Generator技术正在彻底改变代码生成方式。与传统的T4模板或运行时反射不同,Source Generator在编译期间直接介入编译管道,通过分析现有代码结构生成新的C#源文件。这种机制特别适合与partial类型结合使用,形成一种我称之为"partial范式"的开发模式。
1.1 partial类型的工作原理
partial关键字允许我们将一个类、结构或接口的定义拆分到多个文件中。编译器在编译阶段会将这些分散的部分合并为完整的类型定义。这个特性原本是为了方便IDE生成的代码与手写代码分离,但在Source Generator场景下展现出更强大的价值:
csharp复制// 文件1:Person.cs
public partial class Person
{
public string FirstName { get; set; }
}
// 文件2:Person.Generated.cs
public partial class Person
{
public string LastName { get; set; }
}
当Source Generator与partial结合时,可以实现:
- 编译时自动补全类型定义
- 避免运行时反射的性能损耗
- 保持强类型检查的优势
- 生成的代码可随项目一起调试
1.2 Source Generator核心架构
一个典型的Source Generator包含三个关键组件:
- 语法接收器(Syntax Receiver):遍历语法树收集需要生成代码的类型信息
- 生成器(Generator):基于收集的信息生成新代码
- 增量生成器(Incremental Generator):优化性能的增量生成方案
以下是基础生成器的骨架代码:
csharp复制[Generator]
public class DemoGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new DemoSyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxReceiver is not DemoSyntaxReceiver receiver)
return;
// 生成代码逻辑
string sourceCode = GenerateSource(receiver);
context.AddSource("GeneratedCode.cs", SourceText.From(sourceCode, Encoding.UTF8));
}
}
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实战:构建自动DTO生成器
让我们通过一个实际案例展示partial范式的威力 - 自动为模型类生成DTO版本。
2.1 定义输入模型
首先定义需要处理的模型类,注意使用partial:
csharp复制// 原始模型文件
[GenerateDto]
public partial class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
2.2 实现Syntax Receiver
创建语法接收器识别标记了[GenerateDto]的类型:
csharp复制class DtoSyntaxReceiver : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> CandidateClasses { get; } = new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax classDecl &&
classDecl.AttributeLists.Count > 0)
{
var hasAttribute = classDecl.AttributeLists
.SelectMany(al => al.Attributes)
.Any(a => a.Name.ToString() == "GenerateDto");
if (hasAttribute) CandidateClasses.Add(classDecl);
}
}
}
2.3 实现代码生成逻辑
在Generator的Execute方法中生成DTO代码:
csharp复制private string GenerateDtoCode(ClassDeclarationSyntax classDecl)
{
string className = classDecl.Identifier.Text;
string dtoName = $"{className}Dto";
var properties = classDecl.Members
.OfType<PropertyDeclarationSyntax>()
.Select(p => $"public {p.Type} {p.Identifier} {{ get; set; }}");
return $@"
// <auto-generated/>
namespace {GetNamespace(classDecl)}
{{
public partial class {dtoName}
{{
{string.Join("\n ", properties)}
}}
}}";
}
2.4 增量生成优化
对于大型项目,建议使用增量生成器提高性能:
csharp复制[Generator(LanguageNames.CSharp)]
public class DtoIncrementalGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var provider = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (node, _) => node is ClassDeclarationSyntax cds &&
cds.AttributeLists.Count > 0,
transform: (ctx, _) => (ClassDeclarationSyntax)ctx.Node)
.Where(cds => cds.AttributeLists
.SelectMany(al => al.Attributes)
.Any(a => a.Name.ToString() == "GenerateDto"));
context.RegisterSourceOutput(provider, (spc, source) =>
{
var code = GenerateDtoCode(source);
spc.AddSource($"{source.Identifier.Text}Dto.g.cs", code);
});
}
}
3. 测试Source Generator
测试生成的代码是确保稳定性的关键环节,但传统的单元测试方式不适用。我们需要特殊的技术手段。
3.1 测试架构设计
Source Generator测试需要模拟编译管道,推荐使用Microsoft.CodeAnalysis.Testing包:
csharp复制[TestClass]
public class DtoGeneratorTests
{
[TestMethod]
public async Task Should_Generate_Dto_Class()
{
// 准备测试代码
string testCode = @"
[GenerateDto]
public partial class Product
{
public int Id { get; set; }
public string Name { get; set; }
}";
// 创建测试环境
var test = new CSharpSourceGeneratorTest<DtoGenerator, XUnitVerifier>
{
TestState =
{
Sources = { testCode },
GeneratedSources =
{
(typeof(DtoGenerator), "ProductDto.g.cs",
@"// <auto-generated/>
public partial class ProductDto
{
public int Id { get; set; }
public string Name { get; set; }
}")
}
}
};
await test.RunAsync();
}
}
3.2 验证生成结果
除了验证代码存在,还需要检查生成内容是否符合预期:
csharp复制[TestMethod]
public async Task Generated_Dto_Should_Have_All_Properties()
{
string testCode = /* 同上 */;
var test = new CSharpSourceGeneratorTest<DtoGenerator, XUnitVerifier>
{
TestState = { Sources = { testCode } }
};
await test.RunAsync();
var compilation = await test.GetCompilationAsync();
var dtoType = compilation.GetTypeByMetadataName("ProductDto");
Assert.IsNotNull(dtoType);
Assert.AreEqual(2, dtoType.GetMembers().OfType<IPropertySymbol>().Count());
}
3.3 测试诊断信息
良好的生成器应该能报告错误和警告:
csharp复制[TestMethod]
public async Task Should_Report_Error_When_No_Partial()
{
string testCode = @"
[GenerateDto] // 缺少partial
public class Product { }";
var test = new CSharpSourceGeneratorTest<DtoGenerator, XUnitVerifier>
{
TestState = { Sources = { testCode } },
ExpectedDiagnostics =
{
DiagnosticResult.CompilerError("SG0001")
.WithMessage("Type marked with [GenerateDto] must be partial")
}
};
await test.RunAsync();
}
4. 高级应用场景与优化
4.1 处理继承关系
当处理继承层级时,需要特殊处理基类属性:
csharp复制private IEnumerable<PropertyDeclarationSyntax> GetAllProperties(ClassDeclarationSyntax classDecl, SemanticModel model)
{
var current = classDecl;
while (current != null)
{
foreach (var prop in current.Members.OfType<PropertyDeclarationSyntax>())
yield return prop;
var baseType = current.BaseList?.Types.FirstOrDefault();
if (baseType == null) break;
var symbol = model.GetSymbolInfo(baseType.Type).Symbol as INamedTypeSymbol;
current = symbol?.DeclaringSyntaxReferences.FirstOrDefault()?
.GetSyntax() as ClassDeclarationSyntax;
}
}
4.2 性能优化技巧
- 缓存语法分析结果:在SyntaxReceiver中缓存常用信息
- 使用Symbol代替Syntax:语义模型查询比语法分析更快
- 增量生成筛选:只处理发生变化的文件
- 并行处理:对独立类型使用Parallel.ForEach
csharp复制// 并行处理示例
Parallel.ForEach(receiver.CandidateClasses, classDecl =>
{
var source = GenerateDtoCode(classDecl);
context.AddSource($"{classDecl.Identifier.Text}Dto.g.cs", source);
});
4.3 处理206 Partial Content场景
在Web开发中,206状态码表示部分内容响应。我们可以生成对应的处理代码:
csharp复制[GeneratePartialResponse]
public partial class FileService
{
public Stream GetFileContent(string path) { /*...*/ }
}
// 生成代码
public partial class FileService
{
public (Stream Content, long From, long To) GetPartialContent(string path, long from, long to)
{
var stream = GetFileContent(path);
// 实现部分内容逻辑
return (stream, from, to);
}
}
5. 调试与问题排查
5.1 调试Source Generator
- 在Generator项目属性中勾选"调试Source Generator"
- 添加Debugger.Launch()在关键位置
- 使用Diagnostics输出调试信息
csharp复制context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(
"SGDEBUG", "Debug",
$"Processing {classDecl.Identifier.Text}",
"Debug", DiagnosticSeverity.Info, true),
Location.None));
5.2 常见问题解决方案
问题1:生成代码不可见
- 检查文件是否添加到编译上下文(AddSource)
- 确认生成的文件后缀是.g.cs或.generated.cs
- 清理解决方案并重新生成
问题2:类型冲突
- 确保生成的类型名称唯一
- 使用命名空间隔离生成的代码
- 考虑添加[GeneratedCode]特性
问题3:性能低下
- 转换为增量生成器
- 减少语法树遍历次数
- 缓存语义查询结果
问题4:IDE不刷新
- 手动运行"重新分析解决方案"
- 关闭并重新打开解决方案
- 更新Visual Studio/Rider到最新版本
6. 架构最佳实践
6.1 分层设计建议
- 核心层:只包含生成逻辑,不依赖具体框架
- 扩展层:针对特定框架(如ASP.NET Core)的增强
- 适配层:处理不同C#版本的兼容性问题
6.2 版本兼容性处理
使用条件编译处理不同C#版本:
csharp复制#if ROSLYN4_0_OR_GREATER
// 使用增量生成器
public class MyGenerator : IIncrementalGenerator
#else
// 回退到传统生成器
public class MyGenerator : ISourceGenerator
#endif
6.3 与DI容器集成
生成服务注册代码:
csharp复制[GenerateServiceRegistration]
public partial class MyService { }
// 生成代码
public static partial class ServiceRegistration
{
public static IServiceCollection AddMyServices(this IServiceCollection services)
{
services.AddScoped<MyService>();
return services;
}
}
在实际项目中,我发现将Source Generator与partial类型结合使用时,最关键的是保持生成的代码与手写代码的清晰边界。我通常会遵循以下规则:
- 生成的代码只包含机械性、重复性的逻辑
- 业务关键算法永远保留在手写代码中
- 为生成的代码添加明显的标记注释
- 确保生成器有完善的测试覆盖
这种partial范式特别适合处理诸如DTO转换、API客户端生成、验证逻辑等场景。通过合理设计,可以显著减少样板代码,同时保持编译时类型安全和良好的调试体验。
