1. SourceGenerator基础概念解析
SourceGenerator是.NET 5+引入的编译时代码生成技术,它允许开发者在项目编译过程中动态生成C#源代码文件。与传统的T4模板或运行时反射不同,SourceGenerator直接在编译管道中运行,这意味着:
- 零运行时开销:生成的代码与手写代码具有相同性能
- 完全类型安全:生成过程中可以访问项目中的所有类型信息
- 即时反馈:在IDE中即可看到生成的代码,无需实际编译
partial关键字在C#中用于拆分类定义,这是SourceGenerator的理想搭档。通过partial类,我们可以:
- 保持手写代码的整洁性
- 将生成的代码隔离到独立文件
- 避免破坏现有代码结构
典型的SourceGenerator项目结构如下:
code复制MyProject/
├── MyProject.csproj
├── HandWritten.cs # 包含partial类定义
└── Generated/
└── HandWritten.g.cs # 生成的代码文件
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 构建基础SourceGenerator
2.1 创建Generator项目
首先创建.NET Standard 2.0类库项目,需要引用必要的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>
基础Generator类实现:
csharp复制[Generator]
public class MySourceGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// 注册语法接收器或初始化逻辑
}
public void Execute(GeneratorExecutionContext context)
{
// 主要生成逻辑
string sourceCode = @"namespace MyApp {
public partial class MyClass {
public void GeneratedMethod() => Console.WriteLine(""Hello from generated code!"");
}
}";
context.AddSource("MyClass.g.cs", SourceText.From(sourceCode, Encoding.UTF8));
}
}
2.2 调试与开发技巧
开发SourceGenerator时,调试可能比较困难。推荐以下方法:
- 使用Debugger.Launch():
csharp复制#if DEBUG
if (!Debugger.IsAttached) Debugger.Launch();
#endif
- 日志输出:
csharp复制context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(
"SG0001",
"Generator Debug",
$"Compiling {context.Compilation.AssemblyName}",
"Debug",
DiagnosticSeverity.Info,
true),
Location.None));
- 临时文件输出:
csharp复制File.WriteAllText(@"C:\temp\generated.cs", sourceCode);
注意:正式发布前务必移除所有调试代码
3. 高级生成策略与模式
3.1 基于语义模型的代码生成
更强大的Generator会分析项目中的现有代码:
csharp复制public void Execute(GeneratorExecutionContext context)
{
var compilation = context.Compilation;
// 获取所有包含特定特性的类
var targetClasses = compilation.SyntaxTrees
.SelectMany(st => st.GetRoot().DescendantNodes()
.OfType<ClassDeclarationSyntax>())
.Where(cds => cds.AttributeLists.Any(al =>
al.Attributes.Any(a => a.Name.ToString() == "GenerateMethods")))
.Select(cds => compilation.GetSemanticModel(cds.SyntaxTree)
.GetDeclaredSymbol(cds));
foreach (var classSymbol in targetClasses)
{
string source = GenerateClassExtension(classSymbol);
context.AddSource($"{classSymbol.Name}_extensions.cs", source);
}
}
3.2 增量生成器(Incremental Generator)
.NET 6+引入了更高效的增量生成器:
csharp复制[Generator]
public class MyIncrementalGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: (s, _) => s is ClassDeclarationSyntax,
transform: (ctx, _) => (ClassDeclarationSyntax)ctx.Node)
.Where(cds => cds.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)));
context.RegisterSourceOutput(classDeclarations,
(spc, syntax) => GeneratePartialExtension(spc, syntax));
}
private void GeneratePartialExtension(...) { ... }
}
4. 测试SourceGenerator
测试是确保Generator可靠性的关键环节。
4.1 基础测试框架
使用Microsoft.CodeAnalysis.Testing包创建测试:
csharp复制[Test]
public async Task ShouldGeneratePartialMethod()
{
// 准备测试代码
string testCode = @"
namespace Test;
public partial class MyClass { }
";
// 预期生成的代码
string expectedGeneratedCode = @"// <auto-generated/>
namespace Test {
public partial class MyClass {
public void GeneratedMethod() { }
}
}
";
// 创建测试实例
var test = new CSharpSourceGeneratorTest<MySourceGenerator>()
{
TestState =
{
Sources = { testCode },
GeneratedSources =
{
(typeof(MySourceGenerator), "MyClass.g.cs",
SourceText.From(expectedGeneratedCode, Encoding.UTF8))
}
}
};
await test.RunAsync();
}
4.2 高级测试场景
测试Generator对不同输入的反应:
csharp复制[DataTestMethod]
[DataRow("public", "ShouldGenerateForPublicClass")]
[DataRow("internal", "ShouldGenerateForInternalClass")]
[DataRow("", "ShouldNotGenerateForNonPartialClass")]
public async Task TestAccessModifiers(string modifiers, string _)
{
string testCode = $@"
namespace Test;
{modifiers} partial class MyClass {{ }}
";
var test = new CSharpSourceGeneratorTest<MySourceGenerator>
{
TestState = { Sources = { testCode } }
};
if (modifiers.Contains("partial"))
{
test.TestState.GeneratedSources.Add(
(typeof(MySourceGenerator), "MyClass.g.cs",
ExpectedGeneratedCode(modifiers)));
}
await test.RunAsync();
}
5. 实战:构建CRUD代码生成器
让我们实现一个实用的生成器,自动为实体类创建基础CRUD操作。
5.1 定义生成标记
csharp复制[AttributeUsage(AttributeTargets.Class)]
public class GenerateCrudAttribute : Attribute
{
public string RoutePrefix { get; set; }
}
5.2 实现生成逻辑
csharp复制private string GenerateCrudController(INamedTypeSymbol classSymbol)
{
var attribute = classSymbol.GetAttributes()
.First(a => a.AttributeClass?.Name == "GenerateCrudAttribute");
string routePrefix = attribute.NamedArguments
.FirstOrDefault(kvp => kvp.Key == "RoutePrefix").Value.Value?.ToString()
?? classSymbol.Name.ToLowerInvariant();
return $@"// <auto-generated/>
using Microsoft.AspNetCore.Mvc;
namespace {classSymbol.ContainingNamespace}.Controllers;
[ApiController]
[Route(""api/[controller]"")]
public class {classSymbol.Name}Controller : ControllerBase
{{
private readonly IRepository<{classSymbol.Name}> _repository;
public {classSymbol.Name}Controller(IRepository<{classSymbol.Name}> repository)
=> _repository = repository;
[HttpGet(""{routePrefix}"")]
public IActionResult GetAll() => Ok(_repository.GetAll());
[HttpGet(""{routePrefix}/{{id}}"")]
public IActionResult GetById(int id) => Ok(_repository.GetById(id));
// 其他CRUD方法...
}}";
}
5.3 处理常见边界情况
在生成代码时需要处理多种特殊情况:
- 泛型类:
csharp复制if (classSymbol.IsGenericType)
{
string typeParams = string.Join(", ",
classSymbol.TypeParameters.Select(tp => tp.Name));
string constraints = string.Join(" ",
classSymbol.TypeParameters.SelectMany(tp =>
tp.ConstraintTypes.Select(ct =>
$"where {tp.Name} : {ct.Name}")));
// 在生成的代码中包含类型参数和约束
}
- 嵌套类:
csharp复制if (classSymbol.ContainingType != null)
{
// 处理嵌套类情况,可能需要修改生成的命名空间
}
- 已有基类:
csharp复制if (classSymbol.BaseType?.SpecialType != SpecialType.System_Object)
{
// 处理已有继承关系的情况
}
6. 性能优化与最佳实践
6.1 缓存策略
SourceGenerator会在每次编译时运行,因此性能至关重要:
csharp复制private static readonly ConcurrentDictionary<string, string> _cache = new();
public void Execute(GeneratorExecutionContext context)
{
var compilation = context.Compilation;
string cacheKey = compilation.AssemblyName + compilation.SyntaxTrees.Length;
if (_cache.TryGetValue(cacheKey, out var cachedCode))
{
context.AddSource("cached_code.cs", cachedCode);
return;
}
// 生成代码...
_cache.TryAdd(cacheKey, generatedCode);
}
6.2 增量生成技巧
- 仅处理发生变化的文件:
csharp复制var changedTrees = context.Compilation.SyntaxTrees
.Where(st => context.ParseOptionsChanged
|| !_previousTrees.Contains(st.FilePath));
- 按需生成:
csharp复制if (classSymbol.GetAttributes()
.Any(a => a.AttributeClass?.Name == "GenerateCode"))
{
// 仅处理带有特定特性的类
}
6.3 代码风格一致性
确保生成的代码符合项目风格:
- 使用SyntaxFactory构建语法树:
csharp复制var classDecl = SyntaxFactory.ClassDeclaration("GeneratedClass")
.AddModifiers(SyntaxFactory.Token(SyntaxKind.PublicKeyword))
.AddMembers(
SyntaxFactory.MethodDeclaration(
SyntaxFactory.PredefinedType(
SyntaxFactory.Token(SyntaxKind.VoidKeyword)),
"GeneratedMethod")
.WithBody(SyntaxFactory.Block()));
- 应用项目缩进规则:
csharp复制var workspace = new AdhocWorkspace();
var formattedNode = Formatter.Format(
syntaxNode,
workspace,
workspace.Options.WithChangedOption(
FormattingOptions.NewLine, LanguageNames.CSharp, "\n"));
7. 调试与问题排查
7.1 常见问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 生成器未运行 | 项目未正确引用生成器 | 检查<ProjectReference>的OutputItemType="Analyzer" |
| 生成的代码不可见 | IDE缓存问题 | 清除解决方案并重新构建 |
| 编译错误CS0246 | 缺少类型引用 | 在生成器中添加#nullable enable和必要的using |
| 性能低下 | 处理了不必要的文件 | 实现增量生成或添加更严格的过滤条件 |
7.2 高级调试技巧
- 使用Roslyn语法可视化工具:
csharp复制var viewModel = new SyntaxNodeVisualizerViewModel(
await CSharpSyntaxTree.ParseText(sourceCode).GetRootAsync());
new SyntaxNodeVisualizer { DataContext = viewModel }.ShowDialog();
- 分析编译上下文:
csharp复制foreach (var tree in context.Compilation.SyntaxTrees)
{
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor("SGDEBUG", "Debug",
$"Processing {tree.FilePath}", "Debug",
DiagnosticSeverity.Info, true),
Location.None));
}
- 比较语法树:
csharp复制var diff = SyntaxFactory.AreEquivalent(originalTree, modifiedTree);
8. 实际应用案例
8.1 自动注册DI服务
csharp复制public void Execute(GeneratorExecutionContext context)
{
var interfaces = context.Compilation.SyntaxTrees
.SelectMany(st => st.GetRoot().DescendantNodes()
.OfType<InterfaceDeclarationSyntax>())
.Where(i => i.Identifier.Text.EndsWith("Service"))
.Select(i => context.Compilation.GetSemanticModel(i.SyntaxTree)
.GetDeclaredSymbol(i));
var implementations = // 查找对应的实现类...
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated/>");
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
sb.AppendLine("public static class ServiceCollectionExtensions");
sb.AppendLine("{");
sb.AppendLine(" public static IServiceCollection AddApplicationServices(");
sb.AppendLine(" this IServiceCollection services)");
sb.AppendLine(" {");
foreach (var (interfaceType, implType) in interfaces.Zip(implementations))
{
sb.AppendLine($" services.AddScoped<{interfaceType}, {implType}>();");
}
sb.AppendLine(" return services;");
sb.AppendLine(" }");
sb.AppendLine("}");
context.AddSource("ServiceRegistration.g.cs", sb.ToString());
}
8.2 自动生成API客户端
csharp复制private string GenerateApiClient(InterfaceDeclarationSyntax interfaceSyntax)
{
var semanticModel = context.Compilation.GetSemanticModel(interfaceSyntax.SyntaxTree);
var interfaceSymbol = semanticModel.GetDeclaredSymbol(interfaceSyntax);
var sb = new StringBuilder();
sb.AppendLine($"// <auto-generated/>");
sb.AppendLine($"namespace {interfaceSymbol.ContainingNamespace}.Clients;");
sb.AppendLine();
sb.AppendLine($"public class {interfaceSymbol.Name}Client : {interfaceSymbol.Name}");
sb.AppendLine($"{{");
sb.AppendLine($" private readonly HttpClient _httpClient;");
sb.AppendLine();
sb.AppendLine($" public {interfaceSymbol.Name}Client(HttpClient httpClient)");
sb.AppendLine($" => _httpClient = httpClient;");
sb.AppendLine();
foreach (var method in interfaceSymbol.GetMembers().OfType<IMethodSymbol>())
{
sb.AppendLine($" public async {method.ReturnType} {method.Name}(");
sb.AppendLine($" {string.Join(", ", method.Parameters.Select(p => $"{p.Type} {p.Name}"))})");
sb.AppendLine($" {{");
// 生成HTTP调用逻辑...
sb.AppendLine($" }}");
sb.AppendLine();
}
sb.AppendLine($"}}");
return sb.ToString();
}
9. 进阶主题:与AOP集成
SourceGenerator可以用于实现编译时AOP:
9.1 方法拦截
csharp复制private string GenerateInterceptedClass(ClassDeclarationSyntax classSyntax)
{
var semanticModel = context.Compilation.GetSemanticModel(classSyntax.SyntaxTree);
var classSymbol = semanticModel.GetDeclaredSymbol(classSyntax);
return $@"// <auto-generated/>
using System.Diagnostics;
namespace {classSymbol.ContainingNamespace};
public partial class {classSymbol.Name}
{{
{string.Join("\n", classSymbol.GetMembers()
.OfType<IMethodSymbol>()
.Where(m => !m.IsAbstract)
.Select(m => $@" [DebuggerStepThrough]
public new {m.ReturnType} {m.Name}({GetParameters(m)})
{{
Console.WriteLine($""Entering {m.Name}"");
try {(m.ReturnType.SpecialType == SpecialType.System_Void ? "" : "return ")}
base.{m.Name}({string.Join(", ", m.Parameters.Select(p => p.Name))});
finally
{{
Console.WriteLine($""Exiting {m.Name}"");
}}
}}"))}
}}";
}
9.2 属性变更通知
csharp复制private string GenerateINotifyPropertyChanged(INamedTypeSymbol classSymbol)
{
return $@"// <auto-generated/>
using System.ComponentModel;
namespace {classSymbol.ContainingNamespace};
public partial class {classSymbol.Name} : INotifyPropertyChanged
{{
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
{string.Join("\n", classSymbol.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => !p.IsReadOnly)
.Select(p => $@" private {p.Type} _{p.Name.ToLowerInvariant()};
public new {p.Type} {p.Name}
{{
get => _{p.Name.ToLowerInvariant()};
set
{{
if (EqualityComparer<{p.Type}>.Default.Equals(
_{p.Name.ToLowerInvariant()}, value)) return;
_{p.Name.ToLowerInvariant()} = value;
OnPropertyChanged();
}}
}}"))}
}}";
}
10. 发布与分发Generator
10.1 NuGet打包配置
xml复制<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<IsRoslynComponent>true</IsRoslynComponent>
<IncludeBuildOutput>false</IncludeBuildOutput>
<SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>
<DevelopmentDependency>true</DevelopmentDependency>
</PropertyGroup>
<ItemGroup>
<None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true"
PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
</Project>
10.2 版本兼容性策略
- 明确支持的Roslyn版本范围:
xml复制<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="[4.3.1,5.0)" />
</ItemGroup>
- 为不同.NET版本提供特定实现:
csharp复制#if ROSLYN4
// .NET 6特定实现
#elif ROSLYN3
// .NET 5特定实现
#endif
- 在文档中明确版本要求:
csharp复制/// <summary>
/// Requires Roslyn 4.x (included in .NET 6 SDK)
/// </summary>
[Generator(LanguageNames.CSharp)]
public class MyGenerator : IIncrementalGenerator
