Skip to content

快速开始

欢迎来到 Roslyn 源生成器学习指南!本文将带你快速入门源生成器的世界。

推荐学习路径

如果你是零基础学习者,建议从 学习路径 开始,它提供了 8 步渐进式学习指引。

什么是源生成器?

源生成器(Source Generator)是 C# 编译器(Roslyn)的一个强大功能,它允许开发者在编译时检查代码并生成额外的 C# 源文件。

深入了解

想要深入理解源生成器的概念和原理?请阅读 什么是源生成器

工作原理

源生成器在编译流程中的位置:

简单示例对比

❌ 不使用源生成器

每次添加属性都需要手动更新 ToString 方法:

csharp
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
    
    // 需要手动编写和维护
    public override string ToString()
    {
        return $"Person {{ Name = {Name}, Age = {Age}, Email = {Email} }}";
    }
}

// 问题:
// 1. 添加新属性时容易忘记更新 ToString
// 2. 代码重复(每个类都要写)
// 3. 维护成本高

✅ 使用源生成器

只需标记特性,代码自动生成:

csharp
[GenerateToString]  // 使用特性标记
public partial class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
    
    // ToString 方法由源生成器自动生成
    // 添加新属性时自动包含在 ToString 中
}

// 优势:
// 1. 自动生成,不会遗漏
// 2. 代码简洁
// 3. 易于维护

创建第一个源生成器

完整项目结构

MySourceGenerator/
├── MyGenerator/                    # 生成器项目
│   ├── MyGenerator.csproj
│   └── HelloWorldGenerator.cs
└── MyGenerator.Consumer/           # 使用者项目
    ├── MyGenerator.Consumer.csproj
    └── Program.cs

步骤 1: 创建生成器项目

bash
# 创建解决方案
dotnet new sln -n MySourceGenerator

# 创建生成器项目(类库)
dotnet new classlib -n MyGenerator
dotnet sln add MyGenerator/MyGenerator.csproj

# 进入生成器项目目录
cd MyGenerator

步骤 2: 配置项目文件

编辑 MyGenerator.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <!-- 目标框架:netstandard2.0 以获得最大兼容性 -->
    <TargetFramework>netstandard2.0</TargetFramework>
    
    <!-- 语言版本 -->
    <LangVersion>latest</LangVersion>
    
    <!-- 不生成引用程序集 -->
    <GenerateReferenceAssemblyPackage>false</GenerateReferenceAssemblyPackage>
    
    <!-- 启用可空引用类型 -->
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <!-- Roslyn 编译器 API -->
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
    
    <!-- 分析器支持 -->
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
  </ItemGroup>

</Project>

关键配置说明

  • TargetFramework: 使用 netstandard2.0 确保最大兼容性
  • PrivateAssets="all": 防止依赖传递到使用者项目
  • LangVersion: 使用最新的 C# 语言特性

步骤 3: 创建生成器类

创建 HelloWorldGenerator.cs

csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

namespace MyGenerator
{
    /// <summary>
    /// 最简单的源生成器示例
    /// 生成一个静态的 HelloWorld 类
    /// </summary>
    [Generator]
    public class HelloWorldGenerator : ISourceGenerator
    {
        /// <summary>
        /// 初始化方法
        /// 在生成器首次加载时调用一次
        /// </summary>
        public void Initialize(GeneratorInitializationContext context)
        {
            // 本示例中不需要初始化逻辑
            // 在更复杂的生成器中,可以在这里注册语法接收器
        }

        /// <summary>
        /// 执行方法
        /// 每次编译时都会调用
        /// </summary>
        public void Execute(GeneratorExecutionContext context)
        {
            // 生成的源代码
            var sourceCode = @"
using System;

namespace Generated
{
    /// <summary>
    /// 由源生成器自动生成的类
    /// </summary>
    public static class HelloWorld
    {
        /// <summary>
        /// 返回问候消息
        /// </summary>
        public static string SayHello() 
        {
            return ""Hello from Source Generator!"";
        }
        
        /// <summary>
        /// 返回带名字的问候消息
        /// </summary>
        public static string SayHello(string name)
        {
            return $""Hello {name} from Source Generator!"";
        }
        
        /// <summary>
        /// 返回生成时间
        /// </summary>
        public static string GetGeneratedTime()
        {
            return ""Generated at: " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + @""";
        }
    }
}";

            // 将生成的代码添加到编译中
            // 参数1: 文件名(必须唯一,建议使用 .g.cs 后缀)
            // 参数2: 源代码内容
            context.AddSource(
                "HelloWorld.g.cs", 
                SourceText.From(sourceCode, Encoding.UTF8)
            );
        }
    }
}

步骤 4: 创建使用者项目

bash
# 返回解决方案根目录
cd ..

# 创建控制台项目
dotnet new console -n MyGenerator.Consumer
dotnet sln add MyGenerator.Consumer/MyGenerator.Consumer.csproj

步骤 5: 配置使用者项目

编辑 MyGenerator.Consumer.csproj

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <!-- 引用生成器项目 -->
    <!-- OutputItemType="Analyzer" 告诉编译器这是一个分析器/生成器 -->
    <!-- ReferenceOutputAssembly="false" 表示不引用生成器的程序集 -->
    <ProjectReference Include="..\MyGenerator\MyGenerator.csproj" 
                      OutputItemType="Analyzer" 
                      ReferenceOutputAssembly="false" />
  </ItemGroup>

</Project>

步骤 6: 使用生成的代码

编辑 Program.cs

csharp
using System;
using Generated;  // 引用生成的命名空间

class Program
{
    static void Main(string[] args)
    {
        // 调用生成器生成的方法
        Console.WriteLine("=== 源生成器示例 ===\n");
        
        // 基本问候
        Console.WriteLine(HelloWorld.SayHello());
        
        // 带名字的问候
        Console.WriteLine(HelloWorld.SayHello("张三"));
        
        // 显示生成时间
        Console.WriteLine(HelloWorld.GetGeneratedTime());
        
        Console.WriteLine("\n=== 示例完成 ===");
    }
}

项目结构

完整的项目结构如下:

MySourceGenerator/
├── MySourceGenerator.sln           # 解决方案文件
├── MyGenerator/                    # 生成器项目
│   ├── MyGenerator.csproj         # 项目文件
│   └── HelloWorldGenerator.cs     # 生成器实现
└── MyGenerator.Consumer/           # 使用者项目
    ├── MyGenerator.Consumer.csproj # 项目文件
    ├── Program.cs                  # 主程序
    └── obj/                        # 编译输出
        └── Debug/
            └── net8.0/
                └── generated/      # 生成的代码位置
                    └── MyGenerator/
                        └── MyGenerator.HelloWorldGenerator/
                            └── HelloWorld.g.cs  # 生成的文件

完整示例

生成器完整代码

csharp
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

namespace MyGenerator
{
    [Generator]
    public class HelloWorldGenerator : ISourceGenerator
    {
        public void Initialize(GeneratorInitializationContext context)
        {
            // 可以在这里注册语法接收器
            // context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
        }

        public void Execute(GeneratorExecutionContext context)
        {
            // 生成代码
            var source = GenerateCode();
            
            // 添加到编译
            context.AddSource(
                "HelloWorld.g.cs",
                SourceText.From(source, Encoding.UTF8)
            );
        }

        private string GenerateCode()
        {
            return @"
using System;

namespace Generated
{
    public static class HelloWorld
    {
        public static string SayHello() => ""Hello from Source Generator!"";
        public static string SayHello(string name) => $""Hello {name}!"";
        public static string GetGeneratedTime() => DateTime.Now.ToString();
    }
}";
        }
    }
}

运行和调试

编译项目

bash
# 编译整个解决方案
dotnet build

# 或者只编译使用者项目(会自动编译生成器)
dotnet build MyGenerator.Consumer

运行程序

bash
dotnet run --project MyGenerator.Consumer

预期输出

=== 源生成器示例 ===

Hello from Source Generator!
Hello 张三!
Generated at: 2025-01-21 10:30:45

=== 示例完成 ===

查看生成的代码

生成的代码位于:

MyGenerator.Consumer/obj/Debug/net8.0/generated/MyGenerator/MyGenerator.HelloWorldGenerator/HelloWorld.g.cs

在 Visual Studio 中:

  1. 展开项目节点
  2. 展开 DependenciesAnalyzersMyGenerator
  3. 可以看到生成的文件

调试生成器

方法 1: 使用 Debugger.Launch()

在生成器代码中添加:

csharp
public void Execute(GeneratorExecutionContext context)
{
    #if DEBUG
    if (!System.Diagnostics.Debugger.IsAttached)
    {
        System.Diagnostics.Debugger.Launch();
    }
    #endif
    
    // 生成器逻辑...
}

方法 2: 附加到进程

  1. 在 Visual Studio 中打开生成器项目
  2. 在生成器代码中设置断点
  3. 选择 调试附加到进程
  4. 找到 csc.exeVBCSCompiler.exe
  5. 附加调试器
  6. 重新编译使用者项目

常见问题

Q1: 为什么看不到生成的代码?

A: 生成的代码在 obj/ 目录下,默认不显示。解决方法:

  1. 在 Solution Explorer 中点击 显示所有文件
  2. 导航到 obj/Debug/net8.0/generated/
  3. 或者使用命令查看:
bash
dotnet build -v detailed | findstr "Generated"

Q2: 生成器没有执行?

A: 检查以下几点:

  1. ✅ 生成器项目是否正确引用:

    xml
    <ProjectReference Include="..\MyGenerator\MyGenerator.csproj" 
                      OutputItemType="Analyzer" 
                      ReferenceOutputAssembly="false" />
  2. ✅ 生成器类是否有 [Generator] 特性

  3. ✅ 是否实现了 ISourceGenerator 接口

  4. ✅ 清理并重新编译:

    bash
    dotnet clean
    dotnet build

Q3: 如何查看生成器的错误?

A: 使用详细输出:

bash
dotnet build -v detailed

或者在生成器中使用诊断:

csharp
context.ReportDiagnostic(Diagnostic.Create(
    new DiagnosticDescriptor(
        "SG0001",
        "Debug Info",
        "Message: {0}",
        "SourceGenerator",
        DiagnosticSeverity.Info,
        true),
    Location.None,
    "Debug message"));

Q4: 生成的代码有编译错误?

A: 常见原因:

  1. 命名空间冲突 - 使用完全限定名称
  2. 语法错误 - 仔细检查生成的代码
  3. 缺少 using - 确保包含必要的命名空间

Q5: 如何在生成器中访问项目配置?

A: 使用 AnalyzerConfigOptions

csharp
public void Execute(GeneratorExecutionContext context)
{
    // 获取全局配置
    context.AnalyzerConfigOptions.GlobalOptions
        .TryGetValue("build_property.RootNamespace", out var rootNamespace);
}

最佳实践

✅ 推荐做法

csharp
// 1. 使用 SourceText.From 而不是直接传递字符串
context.AddSource(
    "File.g.cs",
    SourceText.From(code, Encoding.UTF8)  // 指定编码
);

// 2. 使用 .g.cs 后缀标识生成的文件
context.AddSource("HelloWorld.g.cs", source);

// 3. 使用 StringBuilder 构建大型代码
var sb = new StringBuilder();
sb.AppendLine("namespace Generated");
sb.AppendLine("{");
// ...
sb.AppendLine("}");

// 4. 添加注释说明代码是自动生成的
var code = @"
// <auto-generated/>
// This file is automatically generated by MyGenerator
namespace Generated { }
";

// 5. 使用 #nullable 指令
var code = @"
#nullable enable
namespace Generated { }
";

❌ 避免的做法

csharp
// 1. 不要生成无效的 C# 代码
context.AddSource("Bad.g.cs", "invalid code");  // 会导致编译错误

// 2. 不要使用相同的文件名
context.AddSource("File.g.cs", code1);
context.AddSource("File.g.cs", code2);  // 冲突!

// 3. 不要在生成器中执行耗时操作
public void Execute(GeneratorExecutionContext context)
{
    Thread.Sleep(5000);  // 会严重影响编译速度
}

// 4. 不要依赖外部文件
var content = File.ReadAllText("template.txt");  // 不可靠

// 5. 不要使用非确定性的代码
var code = $"// Generated at {DateTime.Now}";  // 每次编译都不同

性能优化

csharp
// ✅ 使用增量生成器(推荐)
[Generator]
public class MyGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // 增量管道
    }
}

// ✅ 缓存重复计算的结果
private static readonly Dictionary<string, string> _cache = new();

// ✅ 使用 static 方法避免闭包
context.RegisterSourceOutput(provider, 
    static (spc, data) => Generate(spc, data));

下一步

现在你已经创建了第一个源生成器!接下来可以:

深入学习

查看示例

API 参考

进阶主题

真实使用场景

场景 1: 自动生成 DTO 映射代码

在实际项目中,我们经常需要在不同的数据传输对象(DTO)之间进行映射。手动编写映射代码既繁琐又容易出错。

问题场景

csharp
// 领域模型
public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public DateTime CreatedAt { get; set; }
}

// API DTO
public class UserDto
{
    public int Id { get; set; }
    public string FullName { get; set; }
    public string Email { get; set; }
}

// 手动编写映射代码 - 繁琐且容易出错
public class UserMapper
{
    public static UserDto ToDto(User user)
    {
        return new UserDto
        {
            Id = user.Id,
            FullName = $"{user.FirstName} {user.LastName}",
            Email = user.Email
        };
    }
}

使用源生成器的解决方案

csharp
// 1. 定义映射特性
[AttributeUsage(AttributeTargets.Class)]
public class AutoMapAttribute : Attribute
{
    public Type SourceType { get; }
    public AutoMapAttribute(Type sourceType)
    {
        SourceType = sourceType;
    }
}

// 2. 标记需要映射的类
[AutoMap(typeof(User))]
public partial class UserDto
{
    public int Id { get; set; }
    
    [MapFrom("FirstName", "LastName", Formatter = "FullName")]
    public string FullName { get; set; }
    
    public string Email { get; set; }
}

// 3. 源生成器自动生成映射代码
// 生成的代码(UserDto.g.cs):
public partial class UserDto
{
    public static UserDto FromUser(User source)
    {
        return new UserDto
        {
            Id = source.Id,
            FullName = $"{source.FirstName} {source.LastName}",
            Email = source.Email
        };
    }
}

// 4. 使用生成的映射方法
var user = new User 
{ 
    Id = 1, 
    FirstName = "张", 
    LastName = "三",
    Email = "zhangsan@example.com",
    CreatedAt = DateTime.Now
};

var dto = UserDto.FromUser(user);
Console.WriteLine($"{dto.Id}: {dto.FullName} - {dto.Email}");
// 输出: 1: 张 三 - zhangsan@example.com

优势

  • 减少样板代码:不需要手动编写每个映射方法
  • 类型安全:编译时检查,避免运行时错误
  • 易于维护:添加新属性时自动更新映射
  • 零性能开销:不使用反射,性能与手写代码相同

场景 2: 自动生成 API 客户端

在微服务架构中,我们经常需要调用其他服务的 API。手动编写 HTTP 客户端代码既重复又容易出错。

问题场景

csharp
// 手动编写 API 客户端 - 大量重复代码
public class UserApiClient
{
    private readonly HttpClient _httpClient;
    
    public UserApiClient(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    
    public async Task<User> GetUserAsync(int id)
    {
        var response = await _httpClient.GetAsync($"/api/users/{id}");
        response.EnsureSuccessStatusCode();
        var json = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<User>(json);
    }
    
    public async Task<User> CreateUserAsync(CreateUserRequest request)
    {
        var json = JsonSerializer.Serialize(request);
        var content = new StringContent(json, Encoding.UTF8, "application/json");
        var response = await _httpClient.PostAsync("/api/users", content);
        response.EnsureSuccessStatusCode();
        var responseJson = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<User>(responseJson);
    }
    
    // 更多方法...每个都是重复的模板代码
}

使用源生成器的解决方案

csharp
// 1. 定义 API 接口
[GenerateApiClient(BaseUrl = "https://api.example.com")]
public interface IUserApi
{
    [Get("/api/users/{id}")]
    Task<User> GetUserAsync(int id);
    
    [Post("/api/users")]
    Task<User> CreateUserAsync([Body] CreateUserRequest request);
    
    [Put("/api/users/{id}")]
    Task<User> UpdateUserAsync(int id, [Body] UpdateUserRequest request);
    
    [Delete("/api/users/{id}")]
    Task DeleteUserAsync(int id);
}

// 2. 源生成器自动生成实现
// 生成的代码(IUserApi.g.cs):
public class UserApiClient : IUserApi
{
    private readonly HttpClient _httpClient;
    private readonly string _baseUrl;
    
    public UserApiClient(HttpClient httpClient, string baseUrl = "https://api.example.com")
    {
        _httpClient = httpClient;
        _baseUrl = baseUrl;
    }
    
    public async Task<User> GetUserAsync(int id)
    {
        var url = $"{_baseUrl}/api/users/{id}";
        var response = await _httpClient.GetAsync(url);
        response.EnsureSuccessStatusCode();
        return await JsonSerializer.DeserializeAsync<User>(
            await response.Content.ReadAsStreamAsync());
    }
    
    public async Task<User> CreateUserAsync(CreateUserRequest request)
    {
        var url = $"{_baseUrl}/api/users";
        var json = JsonSerializer.Serialize(request);
        var content = new StringContent(json, Encoding.UTF8, "application/json");
        var response = await _httpClient.PostAsync(url, content);
        response.EnsureSuccessStatusCode();
        return await JsonSerializer.DeserializeAsync<User>(
            await response.Content.ReadAsStreamAsync());
    }
    
    // 其他方法的实现...
}

// 3. 使用生成的客户端
var httpClient = new HttpClient();
var userApi = new UserApiClient(httpClient);

// 调用 API
var user = await userApi.GetUserAsync(1);
Console.WriteLine($"User: {user.FirstName} {user.LastName}");

var newUser = await userApi.CreateUserAsync(new CreateUserRequest
{
    FirstName = "李",
    LastName = "四",
    Email = "lisi@example.com"
});

优势

  • 声明式 API:只需定义接口,实现自动生成
  • 类型安全:编译时检查 URL 和参数
  • 减少错误:避免手动拼接 URL 和序列化错误
  • 易于测试:可以轻松 mock 接口进行单元测试
  • 统一风格:所有 API 调用使用相同的模式

性能考虑

编译时性能

源生成器在编译时运行,因此需要注意编译性能:

✅ 推荐:使用增量生成器

csharp
[Generator]
public class OptimizedGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // 1. 只选择带有特定特性的类
        var classDeclarations = context.SyntaxProvider
            .CreateSyntaxProvider(
                predicate: static (s, _) => IsSyntaxTargetForGeneration(s),
                transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx))
            .Where(static m => m is not null);
        
        // 2. 组合数据并生成代码
        context.RegisterSourceOutput(classDeclarations,
            static (spc, source) => Execute(source, spc));
    }
    
    private static bool IsSyntaxTargetForGeneration(SyntaxNode node)
    {
        // 快速语法检查,避免不必要的语义分析
        return node is ClassDeclarationSyntax classDecl &&
               classDecl.AttributeLists.Count > 0;
    }
    
    private static ClassInfo? GetSemanticTargetForGeneration(
        GeneratorSyntaxContext context)
    {
        var classDecl = (ClassDeclarationSyntax)context.Node;
        
        // 只在需要时进行语义分析
        foreach (var attributeList in classDecl.AttributeLists)
        {
            foreach (var attribute in attributeList.Attributes)
            {
                var symbol = context.SemanticModel.GetSymbolInfo(attribute).Symbol;
                if (symbol?.ContainingType?.Name == "GenerateToStringAttribute")
                {
                    return new ClassInfo(/* ... */);
                }
            }
        }
        
        return null;
    }
}

❌ 避免:传统生成器的性能问题

csharp
[Generator]
public class SlowGenerator : ISourceGenerator
{
    public void Execute(GeneratorExecutionContext context)
    {
        // ❌ 问题:每次编译都分析所有语法树
        foreach (var syntaxTree in context.Compilation.SyntaxTrees)
        {
            var root = syntaxTree.GetRoot();
            var semanticModel = context.Compilation.GetSemanticModel(syntaxTree);
            
            // ❌ 问题:遍历所有节点,即使大部分不相关
            foreach (var node in root.DescendantNodes())
            {
                if (node is ClassDeclarationSyntax classDecl)
                {
                    // ❌ 问题:对每个类都进行语义分析
                    var symbol = semanticModel.GetDeclaredSymbol(classDecl);
                    // 处理...
                }
            }
        }
    }
}

运行时性能

源生成器生成的代码在运行时执行,性能与手写代码相同:

方面反射源生成器手写代码
性能慢(运行时开销)快(编译时生成)
类型安全否(运行时检查)是(编译时检查)
调试困难容易容易
维护中等容易困难
灵活性中等

性能对比示例

csharp
// 测试代码
public class PerformanceTest
{
    private const int Iterations = 1_000_000;
    
    [Benchmark]
    public void ReflectionBased()
    {
        var person = new Person { Name = "张三", Age = 30 };
        for (int i = 0; i < Iterations; i++)
        {
            // 使用反射获取属性值
            var type = person.GetType();
            var nameProperty = type.GetProperty("Name");
            var name = nameProperty.GetValue(person);
        }
    }
    
    [Benchmark]
    public void SourceGeneratorBased()
    {
        var person = new Person { Name = "张三", Age = 30 };
        for (int i = 0; i < Iterations; i++)
        {
            // 使用源生成器生成的代码
            var name = person.Name;  // 直接访问,无反射
        }
    }
}

// 性能结果(示例):
// | Method              | Mean      | Error    | StdDev   |
// |-------------------- |----------:|---------:|---------:|
// | ReflectionBased     | 45.23 ms  | 0.892 ms | 0.834 ms |
// | SourceGeneratorBased|  2.15 ms  | 0.042 ms | 0.039 ms |
//
// 源生成器比反射快 21 倍!

内存使用

csharp
// ✅ 推荐:使用 StringBuilder 构建大型代码
public void Execute(GeneratorExecutionContext context)
{
    var sb = new StringBuilder();
    sb.AppendLine("namespace Generated");
    sb.AppendLine("{");
    
    foreach (var classInfo in GetClassesToGenerate(context))
    {
        sb.AppendLine($"    public partial class {classInfo.Name}");
        sb.AppendLine("    {");
        // 生成方法...
        sb.AppendLine("    }");
    }
    
    sb.AppendLine("}");
    
    context.AddSource("Generated.g.cs", 
        SourceText.From(sb.ToString(), Encoding.UTF8));
}

// ❌ 避免:使用字符串拼接
public void Execute(GeneratorExecutionContext context)
{
    string code = "namespace Generated\n{\n";
    
    foreach (var classInfo in GetClassesToGenerate(context))
    {
        code += $"    public partial class {classInfo.Name}\n";
        code += "    {\n";
        // ❌ 每次拼接都创建新字符串,内存效率低
        code += "    }\n";
    }
    
    code += "}\n";
}

集成到现有项目

步骤 1: 添加生成器到现有项目

如果你有一个现有的项目,想要使用源生成器:

xml
<!-- 在你的项目文件中添加 -->
<ItemGroup>
  <!-- 从 NuGet 安装 -->
  <PackageReference Include="MySourceGenerator" Version="1.0.0" 
                    OutputItemType="Analyzer" 
                    ReferenceOutputAssembly="false" />
  
  <!-- 或者引用本地项目 -->
  <ProjectReference Include="..\MyGenerator\MyGenerator.csproj" 
                    OutputItemType="Analyzer" 
                    ReferenceOutputAssembly="false" />
</ItemGroup>

步骤 2: 配置生成器选项

可以通过 .editorconfigDirectory.Build.props 配置生成器:

ini
# .editorconfig
[*.cs]

# 配置源生成器选项
build_property.MyGenerator_EnableLogging = true
build_property.MyGenerator_OutputPath = Generated
xml
<!-- Directory.Build.props -->
<Project>
  <PropertyGroup>
    <MyGenerator_EnableLogging>true</MyGenerator_EnableLogging>
    <MyGenerator_OutputPath>Generated</MyGenerator_OutputPath>
  </PropertyGroup>
</Project>

步骤 3: 在代码中使用

csharp
// 1. 添加必要的 using
using Generated;

// 2. 使用生成器提供的特性
[GenerateToString]
public partial class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

// 3. 使用生成的代码
var product = new Product 
{ 
    Id = 1, 
    Name = "笔记本电脑", 
    Price = 5999.99m 
};

Console.WriteLine(product.ToString());
// 输出: Product { Id = 1, Name = 笔记本电脑, Price = 5999.99 }

步骤 4: 处理生成的文件

xml
<!-- 可选:将生成的文件包含在源代码管理中 -->
<ItemGroup>
  <Compile Include="obj\**\*.g.cs" Visible="false" />
</ItemGroup>

<!-- 或者:排除生成的文件 -->
<!-- .gitignore -->
obj/
bin/
*.g.cs

故障排除

问题 1: 生成器未运行

症状:编译成功,但没有生成代码

解决方案

  1. 检查项目引用配置:

    xml
    <ProjectReference Include="..\MyGenerator\MyGenerator.csproj" 
                      OutputItemType="Analyzer"  <!-- 必须 -->
                      ReferenceOutputAssembly="false" />  <!-- 必须 -->
  2. 清理并重新构建:

    bash
    dotnet clean
    dotnet build -v detailed
  3. 检查生成器是否有 [Generator] 特性

问题 2: 生成的代码有错误

症状:编译失败,提示生成的代码有语法错误

解决方案

  1. 查看生成的文件:

    bash
    # Windows
    type obj\Debug\net8.0\generated\MyGenerator\*.g.cs
    
    # Linux/Mac
    cat obj/Debug/net8.0/generated/MyGenerator/*.g.cs
  2. 在生成器中添加调试输出:

    csharp
    public void Execute(GeneratorExecutionContext context)
    {
        var code = GenerateCode();
        
        // 添加诊断信息
        context.ReportDiagnostic(Diagnostic.Create(
            new DiagnosticDescriptor(
                "SG0001",
                "Generated Code",
                "Generated: {0}",
                "SourceGenerator",
                DiagnosticSeverity.Info,
                true),
            Location.None,
            code));
        
        context.AddSource("Generated.g.cs", code);
    }

问题 3: IDE 不识别生成的代码

症状:代码可以编译,但 IntelliSense 不显示生成的类型

解决方案

  1. 重启 IDE(Visual Studio / Rider / VS Code)

  2. 清理 IDE 缓存:

    • Visual Studio: 删除 .vs 文件夹
    • Rider: 删除 .idea 文件夹
    • VS Code: 重新加载窗口
  3. 确保使用最新版本的 IDE 和 SDK

问题 4: 生成器性能问题

症状:编译时间显著增加

解决方案

  1. 使用增量生成器(IIncrementalGenerator

  2. 添加性能分析:

    csharp
    public void Execute(GeneratorExecutionContext context)
    {
        var sw = Stopwatch.StartNew();
        
        // 生成代码...
        
        sw.Stop();
        context.ReportDiagnostic(Diagnostic.Create(
            new DiagnosticDescriptor(
                "SG0002",
                "Performance",
                "Generation took {0}ms",
                "SourceGenerator",
                DiagnosticSeverity.Info,
                true),
            Location.None,
            sw.ElapsedMilliseconds));
    }
  3. 优化语法树遍历,使用 SyntaxReceiver 过滤

最佳实践 vs 反模式详细对比

对比 1: 代码生成方式

方面✅ 推荐做法❌ 反模式说明
字符串构建使用 StringBuilder使用字符串拼接StringBuilder 避免创建大量临时字符串对象
代码格式使用原始字符串字面量 @""手动转义特殊字符原始字符串更易读,减少转义错误
命名空间使用完全限定名称依赖 using 语句避免命名空间冲突
文件命名使用 .g.cs 后缀使用普通 .cs 后缀清晰标识生成的文件
编码指定 Encoding.UTF8使用默认编码确保跨平台一致性

示例对比

csharp
// ✅ 推荐做法
public void Execute(GeneratorExecutionContext context)
{
    var sb = new StringBuilder();
    sb.AppendLine("// <auto-generated/>");
    sb.AppendLine("using System;");
    sb.AppendLine();
    sb.AppendLine("namespace Generated");
    sb.AppendLine("{");
    sb.AppendLine("    public class MyClass");
    sb.AppendLine("    {");
    sb.AppendLine("        public string GetMessage() => \"Hello\";");
    sb.AppendLine("    }");
    sb.AppendLine("}");
    
    context.AddSource(
        "MyClass.g.cs",
        SourceText.From(sb.ToString(), Encoding.UTF8));
}

// ❌ 反模式
public void Execute(GeneratorExecutionContext context)
{
    string code = "using System;\n";
    code += "namespace Generated\n";
    code += "{\n";
    code += "    public class MyClass\n";
    code += "    {\n";
    code += "        public string GetMessage() { return \"Hello\"; }\n";
    code += "    }\n";
    code += "}\n";
    
    context.AddSource("MyClass.cs", code);  // 缺少 .g.cs 后缀和编码
}

对比 2: 语法树分析

方面✅ 推荐做法❌ 反模式说明
节点过滤使用 SyntaxReceiver遍历所有节点提前过滤,减少不必要的分析
语义分析只在需要时调用对所有节点调用语义分析开销大,应谨慎使用
缓存缓存重复计算每次重新计算避免重复工作
增量使用 IIncrementalGenerator使用 ISourceGenerator增量生成器性能更好

示例对比

csharp
// ✅ 推荐做法:使用 SyntaxReceiver 过滤
class MySyntaxReceiver : ISyntaxReceiver
{
    public List<ClassDeclarationSyntax> CandidateClasses { get; } = new();
    
    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        // 只收集带有特性的类
        if (syntaxNode is ClassDeclarationSyntax classDecl &&
            classDecl.AttributeLists.Count > 0)
        {
            CandidateClasses.Add(classDecl);
        }
    }
}

[Generator]
public class MyGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
    }
    
    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxReceiver is not MySyntaxReceiver receiver)
            return;
        
        // 只处理候选类
        foreach (var classDecl in receiver.CandidateClasses)
        {
            // 处理...
        }
    }
}

// ❌ 反模式:遍历所有节点
[Generator]
public class SlowGenerator : ISourceGenerator
{
    public void Execute(GeneratorExecutionContext context)
    {
        foreach (var syntaxTree in context.Compilation.SyntaxTrees)
        {
            var root = syntaxTree.GetRoot();
            
            // ❌ 遍历所有节点,包括不相关的
            foreach (var node in root.DescendantNodes())
            {
                if (node is ClassDeclarationSyntax classDecl)
                {
                    // 处理...
                }
            }
        }
    }
}

对比 3: 错误处理

方面✅ 推荐做法❌ 反模式说明
异常处理使用 try-catch不处理异常避免生成器崩溃导致编译失败
诊断报告使用 ReportDiagnostic抛出异常提供友好的错误消息
验证验证输入数据假设数据有效防止生成无效代码
降级提供降级方案直接失败即使出错也尽量生成部分代码

示例对比

csharp
// ✅ 推荐做法:完善的错误处理
public void Execute(GeneratorExecutionContext context)
{
    try
    {
        foreach (var classDecl in GetCandidateClasses(context))
        {
            // 验证类是否符合要求
            if (!IsValidClass(classDecl, out var errorMessage))
            {
                // 报告诊断信息
                context.ReportDiagnostic(Diagnostic.Create(
                    new DiagnosticDescriptor(
                        "SG0001",
                        "Invalid Class",
                        errorMessage,
                        "SourceGenerator",
                        DiagnosticSeverity.Warning,
                        true),
                    classDecl.GetLocation()));
                continue;  // 跳过这个类,继续处理其他类
            }
            
            // 生成代码
            var code = GenerateCode(classDecl);
            context.AddSource($"{classDecl.Identifier}.g.cs", code);
        }
    }
    catch (Exception ex)
    {
        // 记录错误但不中断编译
        context.ReportDiagnostic(Diagnostic.Create(
            new DiagnosticDescriptor(
                "SG0002",
                "Generation Error",
                $"Error generating code: {ex.Message}",
                "SourceGenerator",
                DiagnosticSeverity.Error,
                true),
            Location.None));
    }
}

// ❌ 反模式:不处理错误
public void Execute(GeneratorExecutionContext context)
{
    foreach (var classDecl in GetCandidateClasses(context))
    {
        // ❌ 没有验证
        // ❌ 没有错误处理
        // ❌ 异常会导致整个编译失败
        var code = GenerateCode(classDecl);
        context.AddSource($"{classDecl.Identifier}.g.cs", code);
    }
}

对比 4: 测试策略

方面✅ 推荐做法❌ 反模式说明
单元测试编写完整的单元测试不写测试确保生成器正确性
快照测试使用快照测试验证输出手动检查输出自动化验证生成的代码
集成测试测试生成的代码可编译只测试生成器逻辑确保生成的代码有效
边界测试测试边界情况只测试正常情况发现潜在问题

示例对比

csharp
// ✅ 推荐做法:完整的测试
[Fact]
public void GeneratesCorrectCode_ForSimpleClass()
{
    // Arrange
    var source = @"
        using System;
        
        [GenerateToString]
        public partial class Person
        {
            public string Name { get; set; }
            public int Age { get; set; }
        }";
    
    // Act
    var (diagnostics, output) = TestHelper.GetGeneratedOutput(source);
    
    // Assert
    Assert.Empty(diagnostics);  // 没有错误
    Assert.Single(output);  // 生成了一个文件
    
    var generatedCode = output[0].SourceText.ToString();
    Assert.Contains("public override string ToString()", generatedCode);
    Assert.Contains("Name", generatedCode);
    Assert.Contains("Age", generatedCode);
    
    // 验证生成的代码可以编译
    var compilation = TestHelper.CreateCompilation(source, output);
    var compileDiagnostics = compilation.GetDiagnostics();
    Assert.Empty(compileDiagnostics.Where(d => d.Severity == DiagnosticSeverity.Error));
}

[Fact]
public void ReportsError_ForInvalidClass()
{
    // Arrange
    var source = @"
        [GenerateToString]
        public class Person  // ❌ 缺少 partial 关键字
        {
            public string Name { get; set; }
        }";
    
    // Act
    var (diagnostics, output) = TestHelper.GetGeneratedOutput(source);
    
    // Assert
    Assert.NotEmpty(diagnostics);  // 应该有错误
    Assert.Contains(diagnostics, d => d.Id == "SG0001");
}

// ❌ 反模式:没有测试或测试不充分
// 没有测试代码,依赖手动测试

学习路径建议

根据你的经验水平,我们建议以下学习路径:

初学者路径(1-2 周)

  1. 第 1-2 天:阅读本快速开始指南

    • 理解源生成器的概念
    • 创建第一个 Hello World 生成器
    • 学习基本的调试技巧
  2. 第 3-4 天:学习 Roslyn 基础

    • 了解编译器架构
    • 理解语法树的概念
    • 学习基本的 API 使用
  3. 第 5-7 天:实践 ToString 生成器示例

    • 学习如何分析类结构
    • 生成实用的代码
    • 添加自定义选项
  4. 第 8-10 天:学习 测试和调试

    • 编写单元测试
    • 使用调试工具
    • 处理常见问题
  5. 第 11-14 天:创建自己的生成器

    • 选择一个实际问题
    • 设计生成器
    • 实现和测试

进阶路径(2-4 周)

  1. 第 1 周:深入理解 Roslyn

  2. 第 2 周:性能优化

  3. 第 3 周:复杂场景

  4. 第 4 周:实际项目

    • 在真实项目中应用
    • 发布 NuGet 包
    • 收集用户反馈

专家路径(持续学习)

  1. 高级 API

  2. 最新特性

  3. 贡献开源

    • 为开源项目贡献代码
    • 分享你的经验
    • 帮助其他开发者

常见应用场景总结

源生成器在以下场景特别有用:

1. 减少样板代码

  • ✅ 自动生成 ToString(), Equals(), GetHashCode()
  • ✅ 生成构造函数和 Builder 模式
  • ✅ 生成属性变更通知(INotifyPropertyChanged)
  • ✅ 生成数据验证代码

2. 序列化和反序列化

  • ✅ JSON 序列化器(如 System.Text.Json)
  • ✅ XML 序列化器
  • ✅ 二进制序列化器
  • ✅ 自定义格式序列化

3. ORM 和数据访问

  • ✅ 生成数据库查询代码
  • ✅ 生成实体映射
  • ✅ 生成仓储模式代码
  • ✅ 生成迁移脚本

4. API 客户端

  • ✅ REST API 客户端
  • ✅ GraphQL 客户端
  • ✅ gRPC 客户端
  • ✅ WebSocket 客户端

5. 依赖注入

  • ✅ 自动注册服务
  • ✅ 生成工厂方法
  • ✅ 生成代理类
  • ✅ 生成装饰器

6. 配置和选项

  • ✅ 强类型配置
  • ✅ 配置验证
  • ✅ 配置绑定
  • ✅ 环境变量映射

7. 本地化

  • ✅ 生成资源访问器
  • ✅ 类型安全的本地化
  • ✅ 多语言支持
  • ✅ 格式化辅助方法

8. 性能优化

  • ✅ 替代反射
  • ✅ 编译时计算
  • ✅ 内联优化
  • ✅ 零分配代码

资源和参考

官方文档

开源项目

社区资源

博客和文章

工具和库

总结

恭喜你完成了源生成器的快速开始!现在你应该:

✅ 理解源生成器的概念和优势
✅ 能够创建基本的源生成器
✅ 知道如何调试和测试生成器
✅ 了解最佳实践和常见陷阱
✅ 掌握真实项目中的应用场景

下一步行动

  1. 实践:创建一个解决你实际问题的生成器
  2. 学习:深入学习 Roslyn 基础
  3. 探索:查看更多示例项目
  4. 分享:与社区分享你的经验

记住,源生成器是一个强大的工具,但也需要谨慎使用。始终考虑:

  • 🤔 这个问题真的需要源生成器吗?
  • 🤔 生成的代码是否易于理解和维护?
  • 🤔 性能影响是否可接受?
  • 🤔 是否有更简单的解决方案?

祝你在源生成器的旅程中取得成功!如果遇到问题,请查看常见问题或访问我们的社区资源

🔗 相关资源

深入学习

实战示例

API 参考


最后更新: 2025-01-21

基于 MIT 许可发布