快速开始
欢迎来到 Roslyn 源生成器学习指南!本文将带你快速入门源生成器的世界。
推荐学习路径
如果你是零基础学习者,建议从 学习路径 开始,它提供了 8 步渐进式学习指引。
什么是源生成器?
源生成器(Source Generator)是 C# 编译器(Roslyn)的一个强大功能,它允许开发者在编译时检查代码并生成额外的 C# 源文件。
深入了解
想要深入理解源生成器的概念和原理?请阅读 什么是源生成器。
工作原理
源生成器在编译流程中的位置:
简单示例对比
❌ 不使用源生成器
每次添加属性都需要手动更新 ToString 方法:
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. 维护成本高✅ 使用源生成器
只需标记特性,代码自动生成:
[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: 创建生成器项目
# 创建解决方案
dotnet new sln -n MySourceGenerator
# 创建生成器项目(类库)
dotnet new classlib -n MyGenerator
dotnet sln add MyGenerator/MyGenerator.csproj
# 进入生成器项目目录
cd MyGenerator步骤 2: 配置项目文件
编辑 MyGenerator.csproj:
<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:
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: 创建使用者项目
# 返回解决方案根目录
cd ..
# 创建控制台项目
dotnet new console -n MyGenerator.Consumer
dotnet sln add MyGenerator.Consumer/MyGenerator.Consumer.csproj步骤 5: 配置使用者项目
编辑 MyGenerator.Consumer.csproj:
<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:
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 # 生成的文件完整示例
生成器完整代码
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();
}
}";
}
}
}运行和调试
编译项目
# 编译整个解决方案
dotnet build
# 或者只编译使用者项目(会自动编译生成器)
dotnet build MyGenerator.Consumer运行程序
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 中:
- 展开项目节点
- 展开 Dependencies → Analyzers → MyGenerator
- 可以看到生成的文件
调试生成器
方法 1: 使用 Debugger.Launch()
在生成器代码中添加:
public void Execute(GeneratorExecutionContext context)
{
#if DEBUG
if (!System.Diagnostics.Debugger.IsAttached)
{
System.Diagnostics.Debugger.Launch();
}
#endif
// 生成器逻辑...
}方法 2: 附加到进程
- 在 Visual Studio 中打开生成器项目
- 在生成器代码中设置断点
- 选择 调试 → 附加到进程
- 找到
csc.exe或VBCSCompiler.exe - 附加调试器
- 重新编译使用者项目
常见问题
Q1: 为什么看不到生成的代码?
A: 生成的代码在 obj/ 目录下,默认不显示。解决方法:
- 在 Solution Explorer 中点击 显示所有文件
- 导航到
obj/Debug/net8.0/generated/ - 或者使用命令查看:
dotnet build -v detailed | findstr "Generated"Q2: 生成器没有执行?
A: 检查以下几点:
✅ 生成器项目是否正确引用:
xml<ProjectReference Include="..\MyGenerator\MyGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />✅ 生成器类是否有
[Generator]特性✅ 是否实现了
ISourceGenerator接口✅ 清理并重新编译:
bashdotnet clean dotnet build
Q3: 如何查看生成器的错误?
A: 使用详细输出:
dotnet build -v detailed或者在生成器中使用诊断:
context.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(
"SG0001",
"Debug Info",
"Message: {0}",
"SourceGenerator",
DiagnosticSeverity.Info,
true),
Location.None,
"Debug message"));Q4: 生成的代码有编译错误?
A: 常见原因:
- 命名空间冲突 - 使用完全限定名称
- 语法错误 - 仔细检查生成的代码
- 缺少 using - 确保包含必要的命名空间
Q5: 如何在生成器中访问项目配置?
A: 使用 AnalyzerConfigOptions:
public void Execute(GeneratorExecutionContext context)
{
// 获取全局配置
context.AnalyzerConfigOptions.GlobalOptions
.TryGetValue("build_property.RootNamespace", out var rootNamespace);
}最佳实践
✅ 推荐做法
// 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 { }
";❌ 避免的做法
// 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}"; // 每次编译都不同性能优化
// ✅ 使用增量生成器(推荐)
[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));下一步
现在你已经创建了第一个源生成器!接下来可以:
深入学习
- 了解 Roslyn 原理 - 理解编译器工作原理
- 学习语法树 - 掌握代码结构分析
- 学习语义模型 - 获取类型信息
查看示例
- Hello World 示例 - 最简单的生成器
- 增量生成器示例 - 性能优化
- ToString 生成器 - 实用生成器
API 参考
进阶主题
- 诊断和错误报告 - 提供友好的错误消息
- 测试和调试 - 确保生成器质量
- .NET 10 新特性 - 最新功能
真实使用场景
场景 1: 自动生成 DTO 映射代码
在实际项目中,我们经常需要在不同的数据传输对象(DTO)之间进行映射。手动编写映射代码既繁琐又容易出错。
问题场景
// 领域模型
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
};
}
}使用源生成器的解决方案
// 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 客户端代码既重复又容易出错。
问题场景
// 手动编写 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);
}
// 更多方法...每个都是重复的模板代码
}使用源生成器的解决方案
// 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 调用使用相同的模式
性能考虑
编译时性能
源生成器在编译时运行,因此需要注意编译性能:
✅ 推荐:使用增量生成器
[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;
}
}❌ 避免:传统生成器的性能问题
[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);
// 处理...
}
}
}
}
}运行时性能
源生成器生成的代码在运行时执行,性能与手写代码相同:
| 方面 | 反射 | 源生成器 | 手写代码 |
|---|---|---|---|
| 性能 | 慢(运行时开销) | 快(编译时生成) | 快 |
| 类型安全 | 否(运行时检查) | 是(编译时检查) | 是 |
| 调试 | 困难 | 容易 | 容易 |
| 维护 | 中等 | 容易 | 困难 |
| 灵活性 | 高 | 中等 | 低 |
性能对比示例
// 测试代码
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 倍!内存使用
// ✅ 推荐:使用 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: 添加生成器到现有项目
如果你有一个现有的项目,想要使用源生成器:
<!-- 在你的项目文件中添加 -->
<ItemGroup>
<!-- 从 NuGet 安装 -->
<PackageReference Include="MySourceGenerator" Version="1.0.0"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<!-- 或者引用本地项目 -->
<ProjectReference Include="..\MyGenerator\MyGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>步骤 2: 配置生成器选项
可以通过 .editorconfig 或 Directory.Build.props 配置生成器:
# .editorconfig
[*.cs]
# 配置源生成器选项
build_property.MyGenerator_EnableLogging = true
build_property.MyGenerator_OutputPath = Generated<!-- Directory.Build.props -->
<Project>
<PropertyGroup>
<MyGenerator_EnableLogging>true</MyGenerator_EnableLogging>
<MyGenerator_OutputPath>Generated</MyGenerator_OutputPath>
</PropertyGroup>
</Project>步骤 3: 在代码中使用
// 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: 处理生成的文件
<!-- 可选:将生成的文件包含在源代码管理中 -->
<ItemGroup>
<Compile Include="obj\**\*.g.cs" Visible="false" />
</ItemGroup>
<!-- 或者:排除生成的文件 -->
<!-- .gitignore -->
obj/
bin/
*.g.cs故障排除
问题 1: 生成器未运行
症状:编译成功,但没有生成代码
解决方案:
检查项目引用配置:
xml<ProjectReference Include="..\MyGenerator\MyGenerator.csproj" OutputItemType="Analyzer" <!-- 必须 --> ReferenceOutputAssembly="false" /> <!-- 必须 -->清理并重新构建:
bashdotnet clean dotnet build -v detailed检查生成器是否有
[Generator]特性
问题 2: 生成的代码有错误
症状:编译失败,提示生成的代码有语法错误
解决方案:
查看生成的文件:
bash# Windows type obj\Debug\net8.0\generated\MyGenerator\*.g.cs # Linux/Mac cat obj/Debug/net8.0/generated/MyGenerator/*.g.cs在生成器中添加调试输出:
csharppublic 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 不显示生成的类型
解决方案:
重启 IDE(Visual Studio / Rider / VS Code)
清理 IDE 缓存:
- Visual Studio: 删除
.vs文件夹 - Rider: 删除
.idea文件夹 - VS Code: 重新加载窗口
- Visual Studio: 删除
确保使用最新版本的 IDE 和 SDK
问题 4: 生成器性能问题
症状:编译时间显著增加
解决方案:
使用增量生成器(
IIncrementalGenerator)添加性能分析:
csharppublic 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)); }优化语法树遍历,使用
SyntaxReceiver过滤
最佳实践 vs 反模式详细对比
对比 1: 代码生成方式
| 方面 | ✅ 推荐做法 | ❌ 反模式 | 说明 |
|---|---|---|---|
| 字符串构建 | 使用 StringBuilder | 使用字符串拼接 | StringBuilder 避免创建大量临时字符串对象 |
| 代码格式 | 使用原始字符串字面量 @"" | 手动转义特殊字符 | 原始字符串更易读,减少转义错误 |
| 命名空间 | 使用完全限定名称 | 依赖 using 语句 | 避免命名空间冲突 |
| 文件命名 | 使用 .g.cs 后缀 | 使用普通 .cs 后缀 | 清晰标识生成的文件 |
| 编码 | 指定 Encoding.UTF8 | 使用默认编码 | 确保跨平台一致性 |
示例对比
// ✅ 推荐做法
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 | 增量生成器性能更好 |
示例对比
// ✅ 推荐做法:使用 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 | 抛出异常 | 提供友好的错误消息 |
| 验证 | 验证输入数据 | 假设数据有效 | 防止生成无效代码 |
| 降级 | 提供降级方案 | 直接失败 | 即使出错也尽量生成部分代码 |
示例对比
// ✅ 推荐做法:完善的错误处理
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: 测试策略
| 方面 | ✅ 推荐做法 | ❌ 反模式 | 说明 |
|---|---|---|---|
| 单元测试 | 编写完整的单元测试 | 不写测试 | 确保生成器正确性 |
| 快照测试 | 使用快照测试验证输出 | 手动检查输出 | 自动化验证生成的代码 |
| 集成测试 | 测试生成的代码可编译 | 只测试生成器逻辑 | 确保生成的代码有效 |
| 边界测试 | 测试边界情况 | 只测试正常情况 | 发现潜在问题 |
示例对比
// ✅ 推荐做法:完整的测试
[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-2 天:阅读本快速开始指南
- 理解源生成器的概念
- 创建第一个 Hello World 生成器
- 学习基本的调试技巧
第 3-4 天:学习 Roslyn 基础
- 了解编译器架构
- 理解语法树的概念
- 学习基本的 API 使用
第 5-7 天:实践 ToString 生成器示例
- 学习如何分析类结构
- 生成实用的代码
- 添加自定义选项
第 8-10 天:学习 测试和调试
- 编写单元测试
- 使用调试工具
- 处理常见问题
第 11-14 天:创建自己的生成器
- 选择一个实际问题
- 设计生成器
- 实现和测试
进阶路径(2-4 周)
第 1 周:深入理解 Roslyn
第 2 周:性能优化
第 3 周:复杂场景
- Builder 生成器
- 诊断报告
- 错误处理
第 4 周:实际项目
- 在真实项目中应用
- 发布 NuGet 包
- 收集用户反馈
专家路径(持续学习)
高级 API
最新特性
- .NET 10 新特性
- 关注 Roslyn GitHub 仓库
- 参与社区讨论
贡献开源
- 为开源项目贡献代码
- 分享你的经验
- 帮助其他开发者
常见应用场景总结
源生成器在以下场景特别有用:
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. 性能优化
- ✅ 替代反射
- ✅ 编译时计算
- ✅ 内联优化
- ✅ 零分配代码
资源和参考
官方文档
开源项目
社区资源
博客和文章
工具和库
总结
恭喜你完成了源生成器的快速开始!现在你应该:
✅ 理解源生成器的概念和优势
✅ 能够创建基本的源生成器
✅ 知道如何调试和测试生成器
✅ 了解最佳实践和常见陷阱
✅ 掌握真实项目中的应用场景
下一步行动
记住,源生成器是一个强大的工具,但也需要谨慎使用。始终考虑:
- 🤔 这个问题真的需要源生成器吗?
- 🤔 生成的代码是否易于理解和维护?
- 🤔 性能影响是否可接受?
- 🤔 是否有更简单的解决方案?
祝你在源生成器的旅程中取得成功!如果遇到问题,请查看常见问题或访问我们的社区资源。
🔗 相关资源
深入学习
- 什么是源生成器 - 了解源生成器的概念和原理
- Roslyn 基础 - 学习 Roslyn 编译器的基础知识
- 语法树 - 深入理解语法树的结构和操作
- 语义模型 - 掌握语义模型的使用方法
- 符号系统 - 了解符号系统的详细信息
实战示例
- Hello World 示例 - 最简单的入门示例
- 增量生成器示例 - 学习增量生成器的使用
- 测试示例 - 了解如何测试源生成器
API 参考
最后更新: 2025-01-21