一、源生成器是什么?它能解决什么问题?

想象一下你每天都要写大量重复的样板代码,比如DTO类、API客户端或者简单的CRUD操作。每次修改实体后,都得手动同步修改相关代码,既枯燥又容易出错。这时候,源生成器(Source Generator)就像个贴心小助手,能在编译时自动帮你生成这些代码。

源生成器是Roslyn编译器的一部分,它允许我们在代码编译过程中分析现有代码并生成新的C#源代码文件。这些生成的文件会和手写代码一起参与编译,整个过程对开发者完全透明。

举个例子,假设我们有个简单的用户实体类:

// 技术栈:.NET 6 + C# 10
public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

如果每次都要为这样的类手动编写DTO、验证逻辑等,工作量会很大。源生成器可以帮我们自动完成这些工作。

二、如何创建一个基础的源生成器

让我们从创建一个简单的源生成器开始,它会为标记了特定特性的类自动生成ToString()方法。

首先创建一个类库项目,需要安装必要的NuGet包:

<ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3" />
</ItemGroup>

然后创建我们的生成器:

// 技术栈:.NET 6 + C# 10
[Generator]
public class ToStringGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // 注册一个语法接收器来监听类声明
        context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
    }

    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxReceiver is not SyntaxReceiver receiver)
            return;

        // 获取编译中的语义模型
        var compilation = context.Compilation;
        
        foreach (var classDecl in receiver.CandidateClasses)
        {
            var model = compilation.GetSemanticModel(classDecl.SyntaxTree);
            var typeSymbol = model.GetDeclaredSymbol(classDecl);
            
            // 只为有[GenerateToString]特性的类生成代码
            if (typeSymbol.GetAttributes().Any(ad => 
                ad.AttributeClass?.Name == "GenerateToStringAttribute"))
            {
                string source = GenerateToStringCode(typeSymbol);
                context.AddSource($"{typeSymbol.Name}_ToString.g.cs", source);
            }
        }
    }

    private string GenerateToStringCode(INamedTypeSymbol typeSymbol)
    {
        var properties = typeSymbol.GetMembers()
            .OfType<IPropertySymbol>()
            .Where(p => !p.IsStatic && p.DeclaredAccessibility == Accessibility.Public);
        
        var sb = new StringBuilder();
        sb.AppendLine($"// <auto-generated/>");
        sb.AppendLine($"namespace {typeSymbol.ContainingNamespace.ToDisplayString()}");
        sb.AppendLine("{");
        sb.AppendLine($"    partial class {typeSymbol.Name}");
        sb.AppendLine("    {");
        sb.AppendLine("        public override string ToString()");
        sb.AppendLine("        {");
        sb.AppendLine("            return $\"");
        
        bool first = true;
        foreach (var prop in properties)
        {
            if (!first) sb.Append(", ");
            sb.Append($"{prop.Name}: {{{prop.Name}}}");
            first = false;
        }
        
        sb.AppendLine("\";");
        sb.AppendLine("        }");
        sb.AppendLine("    }");
        sb.AppendLine("}");
        
        return sb.ToString();
    }
}

class SyntaxReceiver : ISyntaxReceiver
{
    public List<ClassDeclarationSyntax> CandidateClasses { get; } = new();

    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        if (syntaxNode is ClassDeclarationSyntax classDecl)
        {
            CandidateClasses.Add(classDecl);
        }
    }
}

使用这个生成器非常简单:

[GenerateToString]
public partial class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
}

// 自动生成的ToString()方法会输出:"Id: {Id}, Name: {Name}, Email: {Email}"

三、更高级的应用:自动生成API客户端

让我们看一个更实用的例子:自动生成API客户端。假设我们有一些用特性标记的接口,想为它们自动生成HttpClient调用代码。

首先定义我们的特性:

// 技术栈:.NET 6 + C# 10
[AttributeUsage(AttributeTargets.Interface)]
public class ApiClientAttribute : Attribute
{
    public string BaseUrl { get; }

    public ApiClientAttribute(string baseUrl)
    {
        BaseUrl = baseUrl;
    }
}

[AttributeUsage(AttributeTargets.Method)]
public class GetAttribute : Attribute
{
    public string Path { get; }

    public GetAttribute(string path)
    {
        Path = path;
    }
}

然后创建一个生成器来识别这些接口并生成客户端代码:

[Generator]
public class ApiClientGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new ApiSyntaxReceiver());
    }

    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxReceiver is not ApiSyntaxReceiver receiver)
            return;

        var compilation = context.Compilation;
        
        foreach (var interfaceDecl in receiver.CandidateInterfaces)
        {
            var model = compilation.GetSemanticModel(interfaceDecl.SyntaxTree);
            var interfaceSymbol = model.GetDeclaredSymbol(interfaceDecl);
            
            var apiClientAttr = interfaceSymbol.GetAttributes()
                .FirstOrDefault(ad => ad.AttributeClass?.Name == "ApiClientAttribute");
                
            if (apiClientAttr == null)
                continue;
                
            string baseUrl = apiClientAttr.ConstructorArguments[0].Value.ToString();
            string source = GenerateApiClient(interfaceSymbol, baseUrl);
            context.AddSource($"{interfaceSymbol.Name}Client.g.cs", source);
        }
    }

    private string GenerateApiClient(INamedTypeSymbol interfaceSymbol, string baseUrl)
    {
        var sb = new StringBuilder();
        sb.AppendLine($"// <auto-generated/>");
        sb.AppendLine("using System.Net.Http;");
        sb.AppendLine("using System.Threading.Tasks;");
        sb.AppendLine();
        sb.AppendLine($"namespace {interfaceSymbol.ContainingNamespace.ToDisplayString()}");
        sb.AppendLine("{");
        sb.AppendLine($"    public class {interfaceSymbol.Name}Client : {interfaceSymbol.ToDisplayString()}");
        sb.AppendLine("    {");
        sb.AppendLine("        private readonly HttpClient _httpClient;");
        sb.AppendLine();
        sb.AppendLine($"        public {interfaceSymbol.Name}Client(HttpClient httpClient)");
        sb.AppendLine("        {");
        sb.AppendLine($"            _httpClient = httpClient;");
        sb.AppendLine($"            _httpClient.BaseAddress = new System.Uri(\"{baseUrl}\");");
        sb.AppendLine("        }");
        sb.AppendLine();
        
        foreach (var method in interfaceSymbol.GetMembers().OfType<IMethodSymbol>())
        {
            var getAttr = method.GetAttributes()
                .FirstOrDefault(ad => ad.AttributeClass?.Name == "GetAttribute");
                
            if (getAttr == null)
                continue;
                
            string path = getAttr.ConstructorArguments[0].Value.ToString();
            
            sb.AppendLine($"        public async {method.ReturnType} {method.Name}()");
            sb.AppendLine("        {");
            sb.AppendLine($"            var response = await _httpClient.GetAsync(\"{path}\");");
            sb.AppendLine($"            response.EnsureSuccessStatusCode();");
            
            if (method.ReturnType is INamedTypeSymbol returnType && 
                returnType.IsGenericType && 
                returnType.ConstructedFrom.SpecialType == SpecialType.System_Threading_Tasks_Task)
            {
                sb.AppendLine($"            return await response.Content.ReadAsAsync<{returnType.TypeArguments[0]}>();");
            }
            else
            {
                sb.AppendLine("            return;");
            }
            
            sb.AppendLine("        }");
            sb.AppendLine();
        }
        
        sb.AppendLine("    }");
        sb.AppendLine("}");
        
        return sb.ToString();
    }
}

使用示例:

[ApiClient("https://api.example.com")]
public interface IUserService
{
    [Get("/api/users")]
    Task<List<User>> GetUsers();
    
    [Get("/api/users/{id}")]
    Task<User> GetUser(int id);
}

// 自动生成的客户端可以直接使用:
var httpClient = new HttpClient();
var userService = new UserServiceClient(httpClient);
var users = await userService.GetUsers();

四、源生成器的应用场景与最佳实践

源生成器最擅长的场景包括:

  1. 减少样板代码:DTO、映射代码、API客户端等
  2. 基于约定生成代码:如Entity Framework的DbContext
  3. 编译时AOP:日志、缓存等横切关注点
  4. 性能优化:避免反射,生成直接调用的代码

优点:

  • 编译时完成,不影响运行时性能
  • 生成的代码可调试
  • 与手写代码无缝集成
  • 强类型检查

缺点:

  • 学习曲线较陡
  • 调试生成器本身比较困难
  • 对项目结构有一定要求

注意事项:

  1. 确保生成器项目是netstandard2.0目标框架
  2. 使用部分类(partial)以便混合手写和生成代码
  3. 生成的代码文件名以.g.cs结尾是约定
  4. 考虑增量生成以提高性能
  5. 处理错误情况,给出明确的诊断信息

最佳实践:

  • 为生成器编写单元测试
  • 提供良好的诊断信息
  • 考虑IDE体验,如智能感知支持
  • 文档化生成器的约定和行为

五、总结与展望

源生成器是C#生态中一个强大的工具,它能显著提升开发效率,减少重复劳动。通过本文的示例,我们看到了如何创建从简单到复杂的生成器。虽然上手有一定难度,但一旦掌握,它能带来巨大的生产力提升。

未来,随着.NET生态的发展,源生成器可能会在更多场景中发挥作用,比如:

  • 更智能的ORM代码生成
  • 领域特定语言(DSL)支持
  • 跨平台代码适配
  • 更高级的编译时优化

如果你经常写重复的模式化代码,不妨尝试用源生成器自动化这个过程。刚开始可能会遇到一些挑战,但长期来看,这种投资绝对值得。