Skip to content

常见模式

源生成器开发中的常见模式和最佳实践

📚 本文档内容

本文档总结了源生成器开发中的常见模式,包括:

  • 基础源生成器模板
  • 基于特性的代码生成
  • 两阶段过滤模式
  • 生成嵌套类
  • 诊断报告模式
  • 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 中只提取必要的信息,不传递 SyntaxNodeISymbol

模式 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);
}

关键点:

  • 使用正确的缩进保持代码可读性
  • 生成流畅接口(返回 thisBuilder
  • 提供静态工厂方法方便使用
  • 使用对象初始化器语法

模式 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 运行生成器
  • 验证生成的代码内容
  • 验证诊断输出
  • 验证编译成功

💡 关键要点

  1. 选择合适的模式

    • 简单场景使用基础模板
    • 复杂场景使用增量生成器
  2. 性能优化

    • 使用两阶段过滤
    • 避免传递大对象
    • 使用静态方法
  3. 代码质量

    • 实现诊断报告
    • 编写单元测试
    • 提供清晰的错误消息
  4. 可维护性

    • 使用数据模型
    • 分离关注点
    • 添加注释和文档

🔗 相关文档


📚 下一步

学习完常见模式后,建议继续学习:

  1. 最佳实践 - 了解性能优化方法
  2. 故障排除 - 解决常见问题

基于 MIT 许可发布