源生成器
C# Source Generators
概述
源生成器(Source Generators)是 C# 9 引入的编译时代码生成技术。它允许你在编译阶段分析现有代码,并自动生成额外的 C# 源文件,这些生成的代码会与原始代码一起参与编译。源生成器不会修改已有代码,而是向编译过程中添加新代码。
为什么需要源生成器?传统上,C# 开发者使用反射在运行时获取类型信息,但反射有性能开销。源生成器将这类工作提前到编译期完成,生成的代码与手写代码一样高效。它还能减少重复的样板代码,比如自动实现 INotifyPropertyChanged、自动注册依赖注入等。
基础概念
ISourceGenerator:源生成器需要实现 ISourceGenerator 接口,包含两个方法:Initialize 用于注册语法接收器,Execute 用于执行代码生成逻辑。
GeneratorAttribute:每个源生成器类必须标注 [Generator] 特性,编译器才能识别它。
语法接收器:通过 ISyntaxReceiver 可以在编译阶段遍历语法树,筛选出感兴趣的语法节点,供 Execute 方法使用。
增量源生成器:.NET 6 引入了 IIncrementalGenerator 接口,它只重新处理变化的部分,大幅提升编译性能。新项目推荐使用增量源生成器。
不修改已有代码:源生成器只能添加新代码,不能修改已有的源文件。这是一个核心设计原则。
快速上手
创建一个最简单的源生成器项目:
# 创建源生成器类库项目
dotnet new classlib -n MySourceGenerator
# 进入项目目录
cd MySourceGenerator
编辑项目文件,设置必要的属性:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- 源生成器必须是 netstandard2.0 -->
<TargetFramework>netstandard2.0</TargetFramework>
<!-- 不自动生成 AssemblyInfo -->
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<!-- 引入 Roslyn 分析器包 -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
</ItemGroup>
</Project>
最简单的源生成器示例:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
namespace MySourceGenerator;
[Generator]
public class HelloGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// 初始化阶段,可以注册语法接收器
// 这里不需要,留空即可
}
public void Execute(GeneratorExecutionContext context)
{
// 生成一个简单的类
var source = @"
namespace Generated;
public static class HelloHelper
{
public static string Greet(string name) => $""你好, {name}!"";
}
";
// 添加生成的源代码
context.AddSource("HelloHelper.g.cs", SourceText.From(source, Encoding.UTF8));
}
}
在目标项目中引用源生成器:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<!-- 引用源生成器项目,注意要加 OutputItemType 和 ReferenceOutputAssembly -->
<ProjectReference Include="..\MySourceGenerator\MySourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>
引用后就可以在代码中使用生成的类:
// 使用源生成器生成的代码
Console.WriteLine(Generated.HelloHelper.Greet("世界"));
// 输出: 你好, 世界!
详细用法
使用语法接收器筛选目标
当需要只对特定类或方法生成代码时,使用语法接收器:
// 定义一个标记特性
[AttributeUsage(AttributeTargets.Class)]
public class AutoNotifyAttribute : Attribute
{
}
// 语法接收器:收集标注了 [AutoNotify] 的类
public class AutoNotifyReceiver : ISyntaxReceiver
{
// 存储收集到的类声明
public List<ClassDeclarationSyntax> CandidateClasses { get; } = new();
// 遍历每个语法节点
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
// 只关注类声明
if (syntaxNode is ClassDeclarationSyntax classDecl)
{
// 检查是否有 [AutoNotify] 特性
var hasAttribute = classDecl.AttributeLists
.SelectMany(al => al.Attributes)
.Any(a => a.Name.ToString().Contains("AutoNotify"));
if (hasAttribute)
{
CandidateClasses.Add(classDecl);
}
}
}
}
// 源生成器
[Generator]
public class AutoNotifyGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// 注册语法接收器
context.RegisterForSyntaxNotifications(() => new AutoNotifyReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
// 获取语法接收器收集的结果
if (context.SyntaxReceiver is not AutoNotifyReceiver receiver)
return;
foreach (var classDecl in receiver.CandidateClasses)
{
// 获取类的模型
var model = context.Compilation.GetSemanticModel(classDecl.SyntaxTree);
var classSymbol = model.GetDeclaredSymbol(classDecl) as INamedTypeSymbol;
if (classSymbol is null) continue;
// 获取命名空间
var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
var className = classSymbol.Name;
// 收集所有字段
var fields = classDecl.Members
.OfType<FieldDeclarationSyntax>()
.Where(f => f.Declaration.Variables.Count == 1);
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated/>");
sb.AppendLine("using System.ComponentModel;");
sb.AppendLine();
sb.AppendLine($"namespace {namespaceName};");
sb.AppendLine();
sb.AppendLine($"public partial class {className} : INotifyPropertyChanged");
sb.AppendLine("{");
sb.AppendLine(" public event PropertyChangedEventHandler? PropertyChanged;");
sb.AppendLine();
sb.AppendLine(" protected void OnPropertyChanged(string name) =>");
sb.AppendLine(" PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));");
// 为每个字段生成属性
foreach (var field in fields)
{
var variable = field.Declaration.Variables.First();
var fieldName = variable.Identifier.Text;
var fieldType = field.Declaration.Type.ToString();
// 将 _name 转为 Name
var propName = fieldName.TrimStart('_');
if (propName.Length > 0)
propName = char.ToUpper(propName[0]) + propName.Substring(1);
sb.AppendLine();
sb.AppendLine($" public {fieldType} {propName}");
sb.AppendLine(" {");
sb.AppendLine($" get => {fieldName};");
sb.AppendLine(" set");
sb.AppendLine(" {");
sb.AppendLine($" if ({fieldName} != value)");
sb.AppendLine(" {");
sb.AppendLine($" {fieldName} = value;");
sb.AppendLine($" OnPropertyChanged(nameof({propName}));");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine(" }");
}
sb.AppendLine("}");
context.AddSource($"{className}.g.cs",
SourceText.From(sb.ToString(), Encoding.UTF8));
}
}
}
使用这个生成器:
// 标记需要生成代码的类
[AutoNotify]
public partial class ViewModel
{
// 私有字段,源生成器会自动生成对应的公共属性
private string _name = "";
private int _age = 0;
private bool _isActive = false;
}
// 编译后,生成的代码等价于:
public partial class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged(string name) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
public string Name
{
get => _name;
set { if (_name != value) { _name = value; OnPropertyChanged(nameof(Name)); } }
}
public int Age
{
get => _age;
set { if (_age != value) { _age = value; OnPropertyChanged(nameof(Age)); } }
}
public bool IsActive
{
get => _isActive;
set { if (_isActive != value) { _isActive = value; OnPropertyChanged(nameof(IsActive)); } }
}
}
增量源生成器
增量源生成器只重新处理变化的部分,性能更好:
using Microsoft.CodeAnalysis;
[Generator]
public class IncrementalHelloGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 定义一个提供器:筛选带有特定特性的类
var classProvider = context.SyntaxProvider
.CreateSyntaxProvider(
// 快速判断:是否是类声明
predicate: (node, _) => node is ClassDeclarationSyntax,
// 详细转换:提取类信息
transform: (ctx, _) =>
{
var classDecl = (ClassDeclarationSyntax)ctx.Node;
var model = ctx.SemanticModel;
var symbol = model.GetDeclaredSymbol(classDecl) as INamedTypeSymbol;
return symbol?.ToDisplayString();
})
.Where(name => name is not null);
// 注册生成逻辑
context.RegisterSourceOutput(classProvider, (productionContext, className) =>
{
var source = $@"
// <auto-generated/>
namespace Generated;
public static class {className!.Replace('.', '_')}Extensions
{{
public static string GetTypeName() => ""{className}"";
}}
";
productionContext.AddSource($"{className}.g.cs", source);
});
}
}
从外部文件生成代码
源生成器可以读取项目中的额外文件来生成代码:
[Generator]
public class ConfigGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context)
{
// 读取项目中的额外文件
foreach (var file in context.AdditionalFiles)
{
// 只处理 .json 配置文件
if (!file.Path.EndsWith(".json"))
continue;
var content = File.ReadAllText(file.Path);
// 解析 JSON 并生成对应的 C# 类
// 这里简化处理,实际应使用 System.Text.Json
var className = Path.GetFileNameWithoutExtension(file.Path);
var source = $@"
// <auto-generated/>
// 从 {file.Path} 生成
public static class {className}Config
{{
public static string RawJson => @""{content.Replace("\"", "\"\"")}"";
}}
";
context.AddSource($"{className}Config.g.cs",
SourceText.From(source, Encoding.UTF8));
}
}
}
在项目文件中声明额外文件:
<ItemGroup>
<AdditionalFiles Include="appsettings.json" />
</ItemGroup>
常见场景
自动注册依赖注入
// 定义标记特性
[AttributeUsage(AttributeTargets.Class)]
public class RegisterServiceAttribute : Attribute
{
public ServiceLifetime Lifetime { get; }
public RegisterServiceAttribute(ServiceLifetime lifetime = ServiceLifetime.Scoped)
{
Lifetime = lifetime;
}
}
// 源生成器自动生成注册代码
[Generator]
public class ServiceRegistrationGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var services = context.SyntaxProvider
.ForAttributeWithMetadataName(
"MyApp.RegisterServiceAttribute",
predicate: (node, _) => node is ClassDeclarationSyntax,
transform: (ctx, _) =>
{
var classSymbol = (INamedTypeSymbol)ctx.TargetSymbol;
var attr = ctx.Attributes.First();
var lifetime = (int)attr.ConstructorArguments[0].Value!;
return (classSymbol.ToDisplayString(), lifetime);
});
context.RegisterSourceOutput(services, (ctx, items) =>
{
// 生成服务注册扩展方法
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated/>");
sb.AppendLine("using Microsoft.Extensions.DependencyInjection;");
sb.AppendLine();
sb.AppendLine("public static class AutoServiceExtensions");
sb.AppendLine("{");
sb.AppendLine(" public static void AddAutoServices(this IServiceCollection services)");
sb.AppendLine(" {");
foreach (var (typeName, lifetime) in items)
{
var method = lifetime switch
{
0 => "AddTransient",
1 => "AddScoped",
2 => "AddSingleton",
_ => "AddTransient"
};
sb.AppendLine($" services.{method}<{typeName}>();");
}
sb.AppendLine(" }");
sb.AppendLine("}");
ctx.AddSource("AutoServiceExtensions.g.cs", sb.ToString());
});
}
}
生成强类型配置类
// 从 JSON 配置文件生成强类型配置类
[Generator]
public class StronglyTypedConfigGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context)
{
foreach (var file in context.AdditionalFiles)
{
if (!file.Path.EndsWith(".settings.json"))
continue;
var name = Path.GetFileNameWithoutExtension(file.Path)
.Replace(".settings", "");
var content = File.ReadAllText(file.Path);
// 简化处理:生成包含原始 JSON 的静态类
var source = $@"
// <auto-generated/>
public static class {name}Settings
{{
public const string Json = @""{content.Replace("\"", "\"\"")}"";
}}
";
context.AddSource($"{name}Settings.g.cs",
SourceText.From(source, Encoding.UTF8));
}
}
}
注意事项
目标框架必须是 netstandard2.0:源生成器项目必须使用 netstandard2.0 目标框架,因为它运行在编译器内部,而编译器使用的是 .NET Framework。
不要在生成器中抛出异常:生成器中抛出的异常会导致编译失败。使用 context.ReportDiagnostic 报告警告或错误,而不是抛出异常。
生成代码的文件名:建议使用 .g.cs 后缀命名生成的文件,这样开发者能区分手写代码和生成代码。
调试源生成器:调试源生成器需要在项目文件中添加 <IsRoslynComponent>true</IsRoslynComponent>,然后使用 Visual Studio 附加到编译器进程。
增量生成器的缓存:增量源生成器通过管道缓存中间结果。确保你的转换函数是纯函数,不依赖外部状态,否则缓存会导致错误。
进阶用法
生成多文件
// 一次 Execute 中可以生成多个文件
public void Execute(GeneratorExecutionContext context)
{
// 生成接口
context.AddSource("IRepository.g.cs", SourceText.From(@"
public interface IRepository<T> where T : class
{
T? GetById(int id);
List<T> GetAll();
void Add(T entity);
void Update(T entity);
void Delete(int id);
}
", Encoding.UTF8));
// 生成实现
context.AddSource("InMemoryRepository.g.cs", SourceText.From(@"
using System.Collections.Generic;
public class InMemoryRepository<T> : IRepository<T> where T : class
{
private readonly List<T> _items = new();
private int _nextId = 1;
public T? GetById(int id) => _items.Count > id ? _items[id] : null;
public List<T> GetAll() => _items;
public void Add(T entity) => _items.Add(entity);
public void Update(T entity) { /* 简化实现 */ }
public void Delete(int id) { /* 简化实现 */ }
}
", Encoding.UTF8));
}
报告诊断信息
// 定义自定义诊断规则
private static readonly DiagnosticDescriptor InvalidFieldRule = new(
id: "MYGEN001",
title: "字段命名不符合规范",
messageFormat: "字段 '{0}' 应以下划线开头",
category: "Naming",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
// 在 Execute 中报告诊断
public void Execute(GeneratorExecutionContext context)
{
foreach (var field in fields)
{
var name = field.Declaration.Variables.First().Identifier.Text;
if (!name.StartsWith("_"))
{
// 报告警告而不是抛出异常
var location = field.GetLocation();
context.ReportDiagnostic(Diagnostic.Create(InvalidFieldRule, location, name));
}
}
}