1. .NET源码生成器项目概述
在.NET生态系统中,源码生成器(Source Generator)是一项革命性的功能,它允许开发者在编译过程中动态生成C#代码。这种技术特别适合解决那些需要大量重复代码但又不适合用运行时反射的场景。partial类(部分类)是C#提供的一种将一个类的定义分散在多个文件中的机制,这为源码生成器提供了完美的对接点。
我最近完成了一个基于partial范式的.NET源码生成器项目,并将其打包为NuGet包供团队使用。这个项目的核心价值在于:
- 自动生成DTO对象的映射代码,减少90%的手写映射代码
- 为领域模型生成验证逻辑,确保业务规则一致性
- 创建API客户端代理类,简化服务调用
- 所有这些都在编译时完成,零运行时开销
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心技术解析
2.1 源码生成器工作原理
.NET源码生成器实际上是实现了ISourceGenerator接口的类库,它在编译管道中插入自己的逻辑。当项目开始编译时:
- 编译器首先解析所有源代码
- 然后调用注册的源码生成器
- 生成器分析代码结构(通过语法树和语义模型)
- 生成新的C#代码文件
- 这些新文件被加入编译过程
整个过程完全在内存中进行,不会产生物理文件(除非特别配置)。这种设计既保持了开发体验的整洁,又提供了强大的代码生成能力。
2.2 partial类的妙用
partial类是这个方案的关键设计点。通过将生成的代码与手写代码分离到不同的partial类定义中,我们实现了:
csharp复制// 手写部分(用户维护)
public partial class Order
{
public int Id { get; set; }
public DateTime OrderDate { get; set; }
}
// 生成部分(自动生成)
public partial class Order
{
public void Validate()
{
if(Id <= 0) throw new ArgumentException("Invalid Id");
if(OrderDate > DateTime.Now) throw new ArgumentException("Invalid OrderDate");
}
}
这种分离确保了:
- 生成的代码不会干扰手写代码
- 手写代码修改不会影响生成逻辑
- 两边可以独立演化
- 智能提示能同时看到两部分成员
3. 开发实战指南
3.1 创建源码生成器项目
首先创建一个.NET类库项目,然后添加必要的NuGet包引用:
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"表示这些包不会传递到引用项目
- 需要同时引用C#分析器和通用分析器
- 版本号应与目标.NET版本匹配
3.2 实现ISourceGenerator接口
核心生成器类的基本结构:
csharp复制[Generator]
public class DtoGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// 注册语法接收器
context.RegisterForSyntaxNotifications(() => new DtoSyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
if (!(context.SyntaxReceiver is DtoSyntaxReceiver receiver))
return;
// 分析收集的类型并生成代码
var codeBuilder = new StringBuilder();
foreach (var classDecl in receiver.CandidateClasses)
{
var model = context.Compilation.GetSemanticModel(classDecl.SyntaxTree);
var typeSymbol = model.GetDeclaredSymbol(classDecl);
// 生成partial类代码
codeBuilder.AppendLine(GeneratePartialClass(typeSymbol));
}
context.AddSource("GeneratedDtos.g.cs",
SourceText.From(codeBuilder.ToString(), Encoding.UTF8));
}
private string GeneratePartialClass(INamedTypeSymbol typeSymbol)
{
// 实际生成逻辑
}
}
3.3 设计语法接收器
语法接收器负责在编译初期扫描代码,找出需要处理的类型:
csharp复制class DtoSyntaxReceiver : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> CandidateClasses { get; } = new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax classDecl &&
classDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
{
CandidateClasses.Add(classDecl);
}
}
}
这个接收器会收集所有标记为partial的类定义,供后续处理。
4. NuGet打包与分发
4.1 配置NuGet包元数据
在项目文件中添加NuGet包属性:
xml复制<PropertyGroup>
<PackageId>YourCompany.SourceGenerators.Dto</PackageId>
<Version>1.0.0</Version>
<Description>Source generator for automatic DTO mapping and validation</Description>
<PackageTags>source-generator;dto;automapping</PackageTags>
<IncludeBuildOutput>false</IncludeBuildOutput>
<DevelopmentDependency>true</DevelopmentDependency>
</PropertyGroup>
关键配置说明:
- IncludeBuildOutput=false 表示不包含程序集输出
- DevelopmentDependency=true 标记为开发时依赖
- 必须设置PackageId和Version
4.2 包含分析器文件
源码生成器需要作为分析器部署,修改项目文件:
xml复制<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true"
PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
这确保生成器DLL被放入正确的分析器目录。
4.3 本地测试与发布
在发布前,可以通过本地NuGet源测试:
bash复制# 打包
dotnet pack --configuration Release
# 添加到本地源
dotnet nuget add source C:\path\to\package\directory -n LocalSource
# 在测试项目中引用
dotnet add package YourCompany.SourceGenerators.Dto --version 1.0.0
测试无误后,使用dotnet nuget push命令发布到NuGet.org或私有仓库。
5. 高级应用场景
5.1 自动接口实现
生成器可以自动为标记接口生成实现:
csharp复制// 用户代码
[AutoImplement]
public partial interface IOrderService
{
Task<Order> GetOrderAsync(int id);
Task SaveOrderAsync(Order order);
}
// 生成代码
public partial class OrderService : IOrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository)
{
_repository = repository;
}
public async Task<Order> GetOrderAsync(int id)
{
return await _repository.GetByIdAsync(id);
}
public async Task SaveOrderAsync(Order order)
{
await _repository.SaveAsync(order);
}
}
5.2 动态查询构建
为实体类生成强类型查询构建器:
csharp复制// 用户查询
var query = new UserQueryBuilder()
.WhereNameContains("john")
.OrderByRegistrationDateDesc()
.Take(10);
// 生成代码
public partial class UserQueryBuilder
{
public UserQueryBuilder WhereNameContains(string value)
{
// 添加条件逻辑
return this;
}
public UserQueryBuilder OrderByRegistrationDateDesc()
{
// 添加排序逻辑
return this;
}
}
6. 性能优化技巧
6.1 增量生成策略
使用增量生成器(IncrementalGenerator)提高性能:
csharp复制[Generator]
public class IncrementalDtoGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var provider = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (node, _) => node is ClassDeclarationSyntax c &&
c.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)),
transform: (ctx, _) => (ClassDeclarationSyntax)ctx.Node)
.Where(c => c is not null);
context.RegisterSourceOutput(provider, (spc, source) =>
{
// 生成代码
});
}
}
6.2 缓存策略
对于复杂的生成逻辑,实现缓存机制:
csharp复制private static readonly ConcurrentDictionary<string, string> _cache = new();
private string GenerateWithCache(INamedTypeSymbol typeSymbol)
{
var cacheKey = $"{typeSymbol.ContainingNamespace}.{typeSymbol.Name}";
return _cache.GetOrAdd(cacheKey, key =>
{
// 复杂生成逻辑
return GenerateComplexCode(typeSymbol);
});
}
7. 调试与问题排查
7.1 调试源码生成器
在项目文件中添加调试配置:
xml复制<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
这会输出生成的代码文件,方便调试。
7.2 常见问题解决
问题1:生成器未被调用
- 检查项目是否引用了生成器项目/包
- 确认生成器类有[Generator]属性
- 检查是否配置了分析器路径
问题2:生成的代码有错误
- 检查语法树处理逻辑
- 验证语义模型获取是否正确
- 确保生成的代码符合C#语法规则
问题3:性能问题
- 使用增量生成器
- 实现缓存机制
- 优化语法树遍历逻辑
8. 最佳实践总结
经过多个项目的实践,我总结了以下经验:
-
关注单一职责:每个生成器只解决一个问题,不要创建"全能"生成器
-
提供逃生舱:为生成的代码提供手动覆盖机制,例如:
csharp复制partial class Order { partial void CustomValidate() { // 用户可以在这里添加自定义验证逻辑 } public void Validate() { // 自动生成的验证 if(Id <= 0) throw...; // 调用用户自定义逻辑 CustomValidate(); } } -
版本兼容:生成器应该向前兼容,旧版本项目应该能使用新版本生成器
-
文档先行:为生成的API提供XML注释,这些注释也可以被生成
-
性能监控:在CI管道中加入生成时间监控,防止性能退化
-
渐进式采用:在大型项目中逐步引入生成代码,而不是一次性替换
-
代码可读性:生成的代码应该尽可能可读,适当添加注释
-
错误处理:生成器应该有良好的错误报告机制,帮助用户发现问题
