1. 项目概述:.NET源码生成器的核心价值
在.NET生态中,源码生成器(Source Generators)正逐渐成为提升开发效率的利器。这个技术允许我们在编译阶段动态生成C#代码,与传统的反射方案相比,它能在编译时完成代码注入,完全避免了运行时性能损耗。而partial类(部分类)的引入,则为源码生成器提供了完美的协作机制——生成代码与手写代码可以和谐共存于同一个类定义中。
我最近在实际项目中深度应用了这套技术方案,通过NuGet包的形式将源码生成器分发给团队使用。这种做法的优势非常明显:开发人员只需安装NuGet包,就能自动获得代码生成能力,无需手动复制粘贴生成的代码文件。下面我将分享这套技术方案的具体实现细节和踩坑经验。
2. 技术架构解析
2.1 源码生成器的工作原理
源码生成器本质上是一个在编译过程中执行的组件。当项目开始编译时,编译器会先执行源码生成器,生成器分析项目中的代码(通过编译器的语法树API),然后输出新的C#源代码文件。这些生成的文件会与项目原有代码一起参与后续的编译过程。
与运行时代码生成相比,这种方案有几个显著优势:
- 编译时就能发现代码错误
- 完全零运行时开销
- 生成的代码可以直接在IDE中查看和调试
2.2 partial类的关键作用
partial类允许我们将一个类的定义拆分到多个文件中。对于源码生成器来说,这是完美的协作机制:
csharp复制// 手写部分
public partial class MyClass {
public void ManualMethod() { ... }
}
// 生成部分
public partial class MyClass {
public void GeneratedMethod() { ... }
}
这种模式使得:
- 手写代码和生成代码完全隔离
- 可以随时重新生成代码而不影响手动编写的逻辑
- IDE能够提供完整的代码补全和导航功能
3. 实现步骤详解
3.1 创建源码生成器项目
首先需要创建一个.NET Standard 2.0类库项目,这是目前源码生成器的标准目标框架。项目文件需要包含以下关键配置:
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.3" PrivateAssets="all" />
</ItemGroup>
</Project>
注意:必须设置EnforceExtendedAnalyzerRules为true,这样才能确保生成器在编译过程中正确执行。
3.2 实现ISourceGenerator接口
核心生成器需要实现Microsoft.CodeAnalysis.ISourceGenerator接口。以下是一个基础模板:
csharp复制[Generator]
public class MySourceGenerator : ISourceGenerator {
public void Initialize(GeneratorInitializationContext context) {
// 注册语法接收器或执行其他初始化
context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context) {
// 主要生成逻辑
if (context.SyntaxReceiver is not MySyntaxReceiver receiver) return;
var sourceCode = BuildSourceCode(receiver);
context.AddSource("GeneratedCode.cs", SourceText.From(sourceCode, Encoding.UTF8));
}
private string BuildSourceCode(MySyntaxReceiver receiver) {
// 实际生成代码的逻辑
return @"// <auto-generated/>
namespace Generated {
public static partial class Helper {
public static void SayHello() => System.Console.WriteLine(""Hello from generated code!"");
}
}";
}
}
3.3 设计语法接收器
语法接收器用于收集项目中的代码信息,供生成器使用:
csharp复制class MySyntaxReceiver : 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</PackageId>
<Version>1.0.0</Version>
<Description>Source generators for common patterns</Description>
<PackageTags>source-generator;productivity</PackageTags>
<IncludeBuildOutput>false</IncludeBuildOutput>
</PropertyGroup>
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true"
PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
关键点:
- IncludeBuildOutput设为false,因为生成器不需要作为常规程序集引用
- 必须将生成器DLL放在analyzers/dotnet/cs路径下,这是编译器的标准查找位置
4.2 本地测试NuGet包
在发布前,可以使用本地NuGet源进行测试:
- 打包项目:
bash复制dotnet pack --configuration Release
- 在测试项目中引用本地包源:
xml复制<PackageReference Include="YourCompany.SourceGenerators" Version="1.0.0" />
- 验证生成代码是否出现在obj/Debug/netX.X目录下的Generated文件夹中
5. 实战技巧与问题排查
5.1 调试源码生成器
调试生成器可能比较棘手,因为它在编译过程中运行。以下是有效的调试方法:
- 在生成器代码中插入Debugger.Launch():
csharp复制public void Execute(GeneratorExecutionContext context) {
System.Diagnostics.Debugger.Launch();
// ...
}
-
编译引用项目时,会弹出调试器附加对话框
-
或者使用VS的调试配置:
json复制"launch": {
"configurations": [
{
"name": "Debug Source Generator",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": ["build", "/p:GenerateDuringBuild=true"],
"cwd": "${workspaceFolder}/TestProject"
}
]
}
5.2 常见问题解决方案
问题1:生成器未执行
- 检查项目是否引用了生成器包
- 确认生成器DLL位于analyzers/dotnet/cs路径
- 检查生成器类是否有[Generator]特性
问题2:无法找到类型符号
- 确保引用了所有必要的程序集:
csharp复制context.Compilation.References.Add(
MetadataReference.CreateFromFile(typeof(SomeType).Assembly.Location));
问题3:生成的代码有错误
- 使用context.ReportDiagnostic报告详细错误
- 检查生成的代码是否包含必要的using语句
- 验证语法树处理逻辑是否正确
6. 高级应用场景
6.1 自动实现接口
源码生成器非常适合用于自动实现样板代码。例如,自动实现INotifyPropertyChanged:
csharp复制// 用户编写的部分
[AutoNotify]
public partial class Person {
public string FirstName { get; set; }
public string LastName { get; set; }
}
// 生成的部分
public partial class Person : INotifyPropertyChanged {
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public string FirstName {
get => _firstName;
set {
if (_firstName != value) {
_firstName = value;
OnPropertyChanged();
}
}
}
private string _firstName;
// 其他属性...
}
6.2 生成API客户端
基于Swagger/OpenAPI规范自动生成强类型API客户端:
csharp复制[ApiClient("https://api.example.com")]
public partial class ExampleApiClient {
[Get("/users/{id}")]
public Task<User> GetUserAsync(int id);
}
// 生成器会根据特性生成实际的HTTP调用代码
这种模式可以大幅减少手动编写API客户端的工作量,同时保持类型安全和可维护性。
7. 性能优化建议
源码生成器在大型项目中可能会影响编译速度。以下优化策略值得考虑:
-
增量生成:实现ISourceGenerator的升级版本IIncrementalGenerator,它提供了更精细的缓存和增量生成支持
-
条件生成:只在必要时生成代码:
csharp复制if (!context.Compilation.SyntaxTrees.Any(st => st.GetText().ToString().Contains("MySpecialAttribute"))) {
return;
}
-
并行处理:对于大型语法树,使用Parallel.ForEach处理独立节点
-
缓存中间结果:将解析结果缓存到临时文件,避免每次编译都重新分析
通过这些优化,我们成功将一个生成器的执行时间从1200ms降低到了200ms左右。
