常见模式
源生成器开发中的常见模式和最佳实践
📚 本文档内容
本文档总结了源生成器开发中的常见模式,包括:
- 基础源生成器模板
- 基于特性的代码生成
- 两阶段过滤模式
- 生成嵌套类
- 诊断报告模式
- ToString 生成模式
- 测试生成器模式
📋 文档信息
| 项目 | 信息 |
|---|---|
| 文档标题 | 常见模式 |
| 所属系列 | 学习指南 |
| 难度级别 | ⭐⭐⭐ 中级 |
| 预计阅读时间 | 40 分钟 |
| 前置知识 | Roslyn API 介绍 |
| 相关文档 | 最佳实践 |
🎯 学习目标
学完本文档后,你将能够:
- ✅ 掌握基础源生成器模板
- ✅ 实现基于特性的代码生成
- ✅ 使用两阶段过滤模式优化性能
- ✅ 生成嵌套类和辅助类
- ✅ 实现诊断报告
- ✅ 编写生成器测试
📑 快速导航
| 章节 | 内容概要 | 跳转链接 |
|---|---|---|
| 基础模板 | 简单的源生成器模板 | 查看 |
| 特性生成 | 基于特性的代码生成 | 查看 |
| 两阶段过滤 | 性能优化的过滤模式 | 查看 |
| 嵌套类 | 生成 Builder 等辅助类 | 查看 |
| 诊断报告 | 验证和报告错误 | 查看 |
| ToString 生成 | 生成格式化输出方法 | 查看 |
| 测试模式 | 为生成器编写测试 | 查看 |
概览
本章节总结了 7 个示例项目中使用的常见模式和技术,并提供可复用的模板代码。
模式 1: 基础源生成器模板
适用场景: 简单的代码生成,不需要分析现有代码
模板代码:
csharp
using Microsoft.CodeAnalysis;
namespace MyGenerator
{
[Generator]
public class SimpleGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// 可选:注册回调
}
public void Execute(GeneratorExecutionContext context)
{
// 生成代码
string sourceCode = @"
namespace Generated
{
public static class GeneratedClass
{
public static void GeneratedMethod()
{
System.Console.WriteLine(""Generated!"");
}
}
}";
// 添加源文件
context.AddSource("GeneratedClass.g.cs", sourceCode);
}
}
}关键点:
- 使用
[Generator]特性标记生成器类 - 实现
ISourceGenerator接口 - 在
Execute方法中生成代码 - 使用
context.AddSource()添加生成的文件
模式 2: 基于特性的代码生成(增量生成器)
适用场景: 根据自定义特性生成代码
模板代码:
csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace MyGenerator
{
[Generator]
public class AttributeBasedGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 步骤 1: 注册特性定义
context.RegisterPostInitializationOutput(ctx =>
{
ctx.AddSource("MyAttribute.g.cs", @"
namespace MyGenerator
{
[System.AttributeUsage(System.AttributeTargets.Class)]
internal sealed class MyAttribute : System.Attribute
{
}
}");
});
// 步骤 2: 使用 ForAttributeWithMetadataName 查找标记的类
var classDeclarations = context.SyntaxProvider
.ForAttributeWithMetadataName(
fullyQualifiedMetadataName: "MyGenerator.MyAttribute",
predicate: static (node, _) => node is ClassDeclarationSyntax,
transform: static (ctx, _) => GetClassInfo(ctx))
.Where(static info => info is not null);
// 步骤 3: 注册源输出
context.RegisterSourceOutput(classDeclarations,
static (spc, classInfo) => GenerateCode(classInfo!, spc));
}
private static ClassInfo? GetClassInfo(GeneratorAttributeSyntaxContext context)
{
var classDecl = (ClassDeclarationSyntax)context.TargetNode;
var classSymbol = (INamedTypeSymbol)context.TargetSymbol;
// 验证:类必须是 partial
if (!classDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
{
return null;
}
// 提取需要的信息
var properties = classSymbol.GetMembers()
.OfType<IPropertySymbol>()
.Where(p => p.DeclaredAccessibility == Accessibility.Public)
.Select(p => new PropertyInfo(p.Name, p.Type.ToDisplayString()))
.ToImmutableArray();
return new ClassInfo(
classSymbol.Name,
classSymbol.ContainingNamespace.ToDisplayString(),
properties);
}
private static void GenerateCode(ClassInfo classInfo, SourceProductionContext context)
{
var sb = new StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine();
sb.AppendLine($"namespace {classInfo.Namespace}");
sb.AppendLine("{");
sb.AppendLine($" partial class {classInfo.ClassName}");
sb.AppendLine(" {");
sb.AppendLine(" // Generated code here");
sb.AppendLine(" }");
sb.AppendLine("}");
context.AddSource($"{classInfo.ClassName}.g.cs", sb.ToString());
}
}
// 数据模型(必须实现 IEquatable 以支持增量缓存)
internal record ClassInfo(
string ClassName,
string Namespace,
ImmutableArray<PropertyInfo> Properties);
internal record PropertyInfo(string Name, string Type);
}关键点:
- 使用
IIncrementalGenerator接口(性能更好) - 使用
RegisterPostInitializationOutput生成特性定义 - 使用
ForAttributeWithMetadataName高效查找标记的类 - 使用
record类型作为数据模型(自动实现IEquatable) - 在
transform中只提取必要的信息,不传递SyntaxNode或ISymbol
模式 3: 两阶段过滤模式
适用场景: 需要自定义过滤逻辑,不能使用 ForAttributeWithMetadataName
模板代码:
csharp
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var provider = context.SyntaxProvider
.CreateSyntaxProvider(
// 阶段 1: 语法过滤(快速,无语义信息)
predicate: static (node, _) => IsSyntaxTarget(node),
// 阶段 2: 语义转换(较慢,有语义信息)
transform: static (ctx, _) => GetSemanticTarget(ctx))
// 过滤掉 null 结果
.Where(static m => m is not null);
context.RegisterSourceOutput(provider,
static (spc, data) => GenerateCode(data!, spc));
}
// 阶段 1: 快速语法检查
private static bool IsSyntaxTarget(SyntaxNode node)
{
// 只检查语法结构,不访问语义模型
return node is FieldDeclarationSyntax field &&
field.AttributeLists.Count > 0;
}
// 阶段 2: 精确语义检查
private static FieldInfo? GetSemanticTarget(GeneratorSyntaxContext context)
{
var fieldDecl = (FieldDeclarationSyntax)context.Node;
foreach (var variable in fieldDecl.Declaration.Variables)
{
var fieldSymbol = context.SemanticModel.GetDeclaredSymbol(variable) as IFieldSymbol;
// 检查是否有目标特性
if (fieldSymbol?.GetAttributes().Any(a =>
a.AttributeClass?.Name == "MyAttribute") == true)
{
return new FieldInfo(
fieldSymbol.Name,
fieldSymbol.Type.ToDisplayString(),
fieldSymbol.ContainingType.Name);
}
}
return null;
}关键点:
- 阶段 1 使用纯语法检查,速度快
- 阶段 2 使用语义模型,精确但较慢
- 只对通过阶段 1 的节点执行阶段 2
- 返回
null表示不符合条件
模式 4: 生成嵌套类
适用场景: 生成 Builder、Proxy、Wrapper 等辅助类
模板代码:
csharp
private static void GenerateBuilderClass(ClassInfo classInfo, SourceProductionContext context)
{
var sb = new StringBuilder();
sb.AppendLine($"namespace {classInfo.Namespace}");
sb.AppendLine("{");
sb.AppendLine($" partial class {classInfo.ClassName}");
sb.AppendLine(" {");
// 生成嵌套的 Builder 类
sb.AppendLine($" public class Builder");
sb.AppendLine(" {");
// 私有字段
foreach (var prop in classInfo.Properties)
{
sb.AppendLine($" private {prop.Type} _{ToCamelCase(prop.Name)};");
}
sb.AppendLine();
// With 方法(流畅接口)
foreach (var prop in classInfo.Properties)
{
sb.AppendLine($" public Builder With{prop.Name}({prop.Type} value)");
sb.AppendLine(" {");
sb.AppendLine($" _{ToCamelCase(prop.Name)} = value;");
sb.AppendLine(" return this;");
sb.AppendLine(" }");
sb.AppendLine();
}
// Build 方法
sb.AppendLine($" public {classInfo.ClassName} Build()");
sb.AppendLine(" {");
sb.AppendLine($" return new {classInfo.ClassName}");
sb.AppendLine(" {");
for (int i = 0; i < classInfo.Properties.Length; i++)
{
var prop = classInfo.Properties[i];
sb.Append($" {prop.Name} = _{ToCamelCase(prop.Name)}");
if (i < classInfo.Properties.Length - 1)
sb.AppendLine(",");
else
sb.AppendLine();
}
sb.AppendLine(" };");
sb.AppendLine(" }");
sb.AppendLine(" }"); // 结束 Builder 类
// 静态工厂方法
sb.AppendLine();
sb.AppendLine(" public static Builder CreateBuilder()");
sb.AppendLine(" {");
sb.AppendLine(" return new Builder();");
sb.AppendLine(" }");
sb.AppendLine(" }"); // 结束外部类
sb.AppendLine("}");
context.AddSource($"{classInfo.ClassName}.Builder.g.cs", sb.ToString());
}
private static string ToCamelCase(string name)
{
if (string.IsNullOrEmpty(name)) return name;
return char.ToLower(name[0]) + name.Substring(1);
}关键点:
- 使用正确的缩进保持代码可读性
- 生成流畅接口(返回
this或Builder) - 提供静态工厂方法方便使用
- 使用对象初始化器语法
模式 5: 诊断报告模式
适用场景: 验证代码并报告错误、警告或提示
模板代码:
csharp
// 步骤 1: 定义诊断描述符
public static class DiagnosticDescriptors
{
public static readonly DiagnosticDescriptor MissingPartialModifier = new(
id: "MYGEN001",
title: "Missing partial modifier",
messageFormat: "Class '{0}' must be declared as partial to use code generation",
category: "MyGenerator",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Classes using generation attributes must be partial.");
public static readonly DiagnosticDescriptor PerformanceWarning = new(
id: "MYGEN002",
title: "Performance warning",
messageFormat: "Consider using readonly for field '{0}'",
category: "MyGenerator",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
}
// 步骤 2: 在生成器中验证和报告
private static void ValidateAndGenerate(ClassInfo classInfo, SourceProductionContext context)
{
// 验证 1: 检查 partial 修饰符
if (!classInfo.IsPartial)
{
var diagnostic = Diagnostic.Create(
DiagnosticDescriptors.MissingPartialModifier,
classInfo.Location,
classInfo.ClassName);
context.ReportDiagnostic(diagnostic);
return; // 错误情况下不生成代码
}
// 验证 2: 性能建议(警告)
foreach (var field in classInfo.Fields)
{
if (!field.IsReadOnly && field.IsPrivate)
{
var diagnostic = Diagnostic.Create(
DiagnosticDescriptors.PerformanceWarning,
field.Location,
field.Name);
context.ReportDiagnostic(diagnostic);
// 警告不阻止代码生成
}
}
// 验证通过,生成代码
GenerateCode(classInfo, context);
}关键点:
- 使用唯一的诊断 ID(建议格式:
PREFIX###) - 选择合适的严重级别(Error, Warning, Info)
- 提供清晰的错误消息和描述
- 错误时不生成代码,警告时可以继续生成
- 包含位置信息以便 IDE 定位
模式 6: ToString 生成模式
适用场景: 生成格式化输出方法
模板代码:
csharp
private static void GenerateToString(ClassInfo classInfo, SourceProductionContext context)
{
var sb = new StringBuilder();
sb.AppendLine($"namespace {classInfo.Namespace}");
sb.AppendLine("{");
sb.AppendLine($" partial class {classInfo.ClassName}");
sb.AppendLine(" {");
sb.AppendLine(" public override string ToString()");
sb.AppendLine(" {");
// 构建格式字符串
sb.Append($" return $\"{classInfo.ClassName} {{ ");
for (int i = 0; i < classInfo.Properties.Length; i++)
{
var prop = classInfo.Properties[i];
sb.Append($"{prop.Name} = {{{prop.Name}}}");
if (i < classInfo.Properties.Length - 1)
sb.Append(", ");
}
sb.AppendLine(" }\";");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine("}");
context.AddSource($"{classInfo.ClassName}.ToString.g.cs", sb.ToString());
}关键点:
- 使用字符串插值
$""生成格式化输出 - 正确处理属性之间的分隔符
- 覆盖
ToString()方法 - 生成可读的输出格式
模式 7: 测试生成器模式
适用场景: 为源生成器编写单元测试
模板代码:
csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
public class MyGeneratorTests
{
// 测试 1: 验证生成的代码
[Fact]
public void Generator_Generates_Expected_Code()
{
var source = @"
using MyGenerator;
namespace TestNamespace
{
[MyAttribute]
public partial class TestClass
{
public string Name { get; set; }
}
}";
var syntaxTree = CSharpSyntaxTree.ParseText(source);
var compilation = CSharpCompilation.Create(
"TestAssembly",
new[] { syntaxTree },
new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location) });
var generator = new MyGenerator();
var driver = CSharpGeneratorDriver.Create(generator);
driver = driver.RunGeneratorsAndUpdateCompilation(
compilation,
out var outputCompilation,
out var diagnostics);
// 验证没有错误
Assert.Empty(outputCompilation.GetDiagnostics()
.Where(d => d.Severity == DiagnosticSeverity.Error));
// 验证生成了文件
var runResult = driver.GetRunResult();
Assert.Single(runResult.GeneratedTrees);
}
}关键点:
- 使用
CSharpGeneratorDriver运行生成器 - 验证生成的代码内容
- 验证诊断输出
- 验证编译成功
💡 关键要点
选择合适的模式
- 简单场景使用基础模板
- 复杂场景使用增量生成器
性能优化
- 使用两阶段过滤
- 避免传递大对象
- 使用静态方法
代码质量
- 实现诊断报告
- 编写单元测试
- 提供清晰的错误消息
可维护性
- 使用数据模型
- 分离关注点
- 添加注释和文档
🔗 相关文档
- Roslyn API 介绍 - API 基础概念
- 最佳实践 - 性能优化建议
- 故障排除 - 解决常见问题
- 返回学习指南
📚 下一步
学习完常见模式后,建议继续学习: