1. Roslyn Source Generator 核心机制解析
Source Generator 是 Roslyn 编译器提供的一项革命性功能,它允许开发者在编译期间动态生成 C# 源代码。与传统的代码生成工具不同,Source Generator 深度集成在编译管线中,可以直接访问项目的完整语法树和语义模型。
1.1 编译管线中的执行时机
Source Generator 的执行发生在编译过程的特定阶段:
- 初始解析阶段:Roslyn 将源代码解析为语法树(SyntaxTree)
- 语义分析阶段:建立符号表和类型系统
- 生成器执行阶段:此时 Source Generator 被调用,可以:
- 分析现有代码结构
- 基于分析结果生成新代码
- 将生成的代码注入编译流程
- 最终编译阶段:所有代码(原始+生成)一起编译为IL
这种深度集成意味着生成器可以基于完整的项目上下文做出智能决策,而不仅仅是简单的模板替换。
1.2 ISourceGenerator 接口剖析
每个 Source Generator 都必须实现 ISourceGenerator 接口,该接口定义了两个关键方法:
csharp复制public interface ISourceGenerator
{
void Initialize(GeneratorInitializationContext context);
void Execute(GeneratorExecutionContext context);
}
Initialize 方法通常用于:
- 注册语法接收器(Syntax Receiver)
- 预生成必要的辅助类型(如特性类)
- 设置生成器所需的初始状态
Execute 方法是生成器的核心,在这里可以:
- 访问通过语法接收器收集的节点
- 使用
context.Compilation获取完整的语义模型 - 通过
context.AddSource()注入生成的代码
2. 实战:构建自动依赖注入系统
2.1 设计思路与架构
我们将实现一个编译时自动依赖注入系统,核心需求包括:
- 通过
[AutoInjectScoped]特性标记需要注入的服务类 - 自动生成服务注册扩展方法
- 生成的代码应该:
- 包含所有标记类的注册语句
- 正确处理命名空间和类型引用
- 支持方法链式调用
与传统运行时反射方案相比,这种设计具有以下优势:
- 零运行时开销
- 编译时类型安全
- 更好的AOT兼容性
- 生成的代码可调试
2.2 核心实现步骤
2.2.1 定义标记特性
首先需要生成一个用于标记服务类的特性:
csharp复制[Generator]
public class CustomGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
const string attributeSource = @"
[System.AttributeUsage(System.AttributeTargets.Class)]
internal class AutoInjectScopedAttribute : System.Attribute
{
public ServiceLifetime Lifetime { get; set; } = ServiceLifetime.Scoped;
}";
context.RegisterForPostInitialization(ctx =>
ctx.AddSource("AutoInjectScopedAttribute.g.cs", attributeSource));
context.RegisterForSyntaxNotifications(() => new ServiceRegistrationReceiver());
}
}
这里使用 RegisterForPostInitialization 提前注入特性定义,确保后续分析时编译器能识别这个特性。
2.2.2 实现语法接收器
语法接收器负责收集所有候选类声明:
csharp复制internal class ServiceRegistrationReceiver : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> CandidateClasses { get; } = new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax classDecl &&
classDecl.AttributeLists.Count > 0)
{
CandidateClasses.Add(classDecl);
}
}
}
这个接收器会收集所有带有特性的类声明,供后续处理使用。
2.2.3 生成服务注册代码
在 Execute 方法中实现核心生成逻辑:
csharp复制public void Execute(GeneratorExecutionContext context)
{
if (!(context.SyntaxReceiver is ServiceRegistrationReceiver receiver))
return;
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated/>");
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
// 分组处理不同命名空间的类
var groups = receiver.CandidateClasses
.Select(c => context.Compilation.GetSemanticModel(c.SyntaxTree).GetDeclaredSymbol(c))
.Where(s => s != null && s.GetAttributes()
.Any(a => a.AttributeClass?.Name == "AutoInjectScopedAttribute"))
.GroupBy(s => s.ContainingNamespace.ToDisplayString());
foreach (var group in groups)
{
sb.AppendLine($"namespace {group.Key}");
sb.AppendLine("{");
sb.AppendLine(" public static class DependencyInjection");
sb.AppendLine(" {");
sb.AppendLine(" public static IServiceCollection AddAutoRegisteredServices(this IServiceCollection services)");
sb.AppendLine(" {");
foreach (var classSymbol in group)
{
var attribute = classSymbol.GetAttributes()
.First(a => a.AttributeClass?.Name == "AutoInjectScopedAttribute");
var lifetime = attribute.NamedArguments
.FirstOrDefault(a => a.Key == "Lifetime").Value.Value
?? ServiceLifetime.Scoped;
sb.AppendLine($" services.Add{lifetime}<{classSymbol.ToDisplayString()}>();");
}
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine("}");
}
context.AddSource("AutoRegistration.g.cs", sb.ToString());
}
3. 高级应用场景与优化
3.1 性能优化技巧
-
增量生成:实现
IIncrementalGenerator接口可以显著提升性能csharp复制[Generator] public class IncrementalAutoInjectGenerator : IIncrementalGenerator { public void Initialize(IncrementalGeneratorInitializationContext context) { var provider = context.SyntaxProvider .CreateSyntaxProvider( predicate: (node, _) => node is ClassDeclarationSyntax c && c.AttributeLists.Count > 0, transform: (ctx, _) => (ClassDeclarationSyntax)ctx.Node) .Where(c => c != null); context.RegisterSourceOutput(provider, (spc, source) => { // 生成逻辑... }); } } -
缓存策略:对语义模型查询结果进行缓存,避免重复计算
-
并行处理:对独立的任务使用并行处理
3.2 复杂场景处理
接口自动注册:
csharp复制services.AddScoped<IMyService, MyService>();
泛型类型注册:
csharp复制services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
条件注册:
csharp复制if (context.Compilation.GetTypeByMetadataName("SpecialFeature") != null)
{
// 注册特殊功能相关服务
}
4. 调试与问题排查
4.1 调试生成器
-
在生成器项目中添加调试配置:
json复制{ "type": "coreclr", "request": "launch", "preLaunchTask": "build", "program": "dotnet", "args": ["build", "/p:GenerateDuringBuild=true"], "cwd": "${workspaceFolder}/TestProject" } -
使用
Debugger.Launch()在生成器中触发调试:csharp复制if (!Debugger.IsAttached) Debugger.Launch();
4.2 常见问题解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 生成器未执行 | 项目未正确引用生成器 | 确保项目文件包含:<ProjectReference Include="..." OutputItemType="Analyzer" /> |
| 特性未识别 | 特性定义未提前生成 | 使用 RegisterForPostInitialization 提前注册特性 |
| 类型符号为null | 语法节点到符号转换失败 | 检查 GetDeclaredSymbol 调用,确保语法树有效 |
| 生成代码编译错误 | 缺少using指令 | 在生成代码中添加必要的命名空间引用 |
4.3 生成代码审查技巧
-
启用生成代码输出:
xml复制<PropertyGroup> <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath> </PropertyGroup> -
使用
#pragma warning disable抑制生成代码中的警告 -
添加版本标记便于追踪:
csharp复制// <auto-generated version="1.0.0"/>
5. 架构思考与最佳实践
5.1 设计原则
- 单一职责:每个生成器只解决一个特定问题
- 显式优于隐式:生成的代码应该容易被理解
- 最小惊讶原则:生成器的行为应该符合开发者预期
- 可观测性:提供清晰的错误信息和日志
5.2 性能考量
- 生成代码量:避免生成过多不必要代码
- 分析范围:只分析必要的语法节点
- 缓存策略:合理缓存语义查询结果
- 增量编译:利用
IIncrementalGenerator优化性能
5.3 安全实践
- 输入验证:严格验证所有输入数据
- 沙箱执行:考虑在隔离环境中执行生成器
- 代码审查:定期审查生成器代码
- 签名验证:对生成器程序集进行强名称签名
在实际项目中,我们通过Source Generator将原本需要200+行的手动服务注册代码缩减为简单的特性标记,编译时间仅增加约5%,但彻底消除了运行时反射开销,在AOT编译场景下性能提升显著。