1. Source Generator与partial范式解析
在.NET生态中,Source Generator作为编译时元编程的利器,正在改变开发者处理样板代码的方式。partial关键字作为C#语言的核心特性,与Source Generator的结合形成了高效的开发范式。这种组合特别适合解决以下场景:
- 需要自动生成重复性代码(如DTO、API客户端)
- 需要扩展已有类型但无法修改源码
- 需要基于约定生成特定模式代码
我最近在开发一个高性能序列化工具时,就采用了这种模式。通过Source Generator自动生成partial类中的序列化逻辑,相比传统反射方案性能提升近20倍。下面通过具体案例拆解实现要点。
2. 核心实现原理与技术细节
2.1 partial类的工作机制
partial类允许将类的定义拆分到多个文件中,编译时会被合并为单一类型。关键约束包括:
- 所有partial部分必须使用相同可访问性
- 任何部分包含基类声明时,必须一致
- 特性会合并到最终类型
csharp复制// 用户手写部分
[Serializable]
public partial class UserModel
{
public string Name { get; set; }
}
// 生成器生成部分
public partial class UserModel
{
public void Serialize(Stream output)
{
// 自动生成的序列化逻辑
}
}
2.2 Source Generator执行流程
Source Generator在编译管道中的工作阶段:
- 编译器解析所有源码
- 运行所有注册的Source Generator
- 将生成内容加入编译
- 继续后续编译阶段
典型生成器结构示例:
csharp复制[Generator]
public class ModelGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() =>
new ModelSyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxReceiver is not ModelSyntaxReceiver receiver)
return;
// 生成代码逻辑
string source = GeneratePartialClasses(receiver);
context.AddSource("Models.g.cs", SourceText.From(source));
}
}
3. 完整实现与测试方案
3.1 开发环境配置
项目文件需包含以下配置:
xml复制<PropertyGroup>
<LangVersion>preview</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.3" PrivateAssets="all" />
</ItemGroup>
3.2 典型生成器实现
以自动生成DTO的With方法为例:
csharp复制private string GenerateWithMethods(ClassDeclarationSyntax classDecl)
{
var props = classDecl.Members
.OfType<PropertyDeclarationSyntax>()
.Where(p => p.Modifiers.Any(m => m.IsKind(SyntaxKind.PublicKeyword)));
var sb = new StringBuilder();
foreach (var prop in props)
{
sb.AppendLine($@"
public {classDecl.Identifier} With{prop.Identifier}({prop.Type} value)
{{
this.{prop.Identifier} = value;
return this;
}}");
}
return sb.ToString();
}
3.3 测试策略与工具
Source Generator测试的特殊性在于需要模拟编译上下文。推荐使用Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing:
csharp复制[Test]
public async Task Should_Generate_WithMethods()
{
string userCode = @"
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
}";
var generated = await new SourceGeneratorTest<ModelGenerator>()
.WithSource(userCode)
.GetGeneratedOutputAsync();
generated.Should().Contain("WithName");
generated.Should().Contain("WithAge");
}
4. 实战经验与性能优化
4.1 增量生成模式
对于大型项目,应采用增量生成避免重复工作:
csharp复制[Generator(LanguageNames.CSharp)]
public class IncrementalGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var provider = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (node, _) => node is ClassDeclarationSyntax,
transform: (ctx, _) => (ClassDeclarationSyntax)ctx.Node)
.Where(c => c.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)));
context.RegisterSourceOutput(provider, (spc, classDecl) =>
{
spc.AddSource($"{classDecl.Identifier}.g.cs",
GeneratePartialClass(classDecl));
});
}
}
4.2 常见问题排查
-
生成代码不可见:
- 检查obj/Debug/netX.X目录下的generated文件夹
- 确保项目文件配置正确
-
类型解析失败:
- 使用context.Compilation.GetTypeByMetadataName
- 确保依赖项已正确引用
-
性能优化技巧:
- 避免在Execute中做繁重计算
- 使用Symbol替代SyntaxNode进行类型检查
- 对大型项目启用分层编译
5. 高级应用场景
5.1 跨程序集生成方案
当需要跨程序集生成代码时,可采用以下模式:
- 定义公共接口在共享程序集
- 生成器扫描实现该接口的类型
- 生成适配器代码
csharp复制// 共享程序集
public interface IAutoSerializable
{
void Serialize(Stream stream);
}
// 生成器逻辑
var serializableTypes = context.Compilation.SyntaxTrees
.SelectMany(st => st.GetRoot().DescendantNodes())
.OfType<ClassDeclarationSyntax>()
.Where(c => c.BaseList?.Types.Any(t =>
t.ToString() == "IAutoSerializable") ?? false);
5.2 编译时代码验证
通过Diagnostic报告代码问题:
csharp复制context.ReportDiagnostic(Diagnostic.Create(
descriptor: new DiagnosticDescriptor(
"SG001",
"Invalid model definition",
"Model classes must be partial",
"Design",
DiagnosticSeverity.Error,
isEnabledByDefault: true),
location: classDecl.GetLocation()));
在实际项目中,这种模式帮助我们早期发现了约15%的模型定义问题,显著减少了运行时错误。