1. 介绍
老实说:大多数人对代码生成的看法差不多像看蛋切片器——有用,但很少必需。但现在的 .NET 项目越来越复杂,把重复性或模板化的工作自动化,不只是个“花活”,而是提升质量和效率的重要工具。
Source Generators 是从 C# 9 和 .NET 5 开始出现的机制。它们像编译阶段的超人,可以生成 C# 代码,然后作为你项目的一部分被编译。它们不会在运行时改动已编译的程序集,只是在编译前给项目补充新的源码文件。
下面列出一些典型的 Source Generators 使用场景。
自动生成模板化代码
很多项目里需要写大量同质化代码:构造函数、ToString 方法、序列化器、property change notification(INotifyPropertyChanged)等等。Source Generators 能自动生成这些代码,帮开发者省去重复工作和打字错误的风险。
示例:生成器为 ToString
假设你在写 DTO 类(Data Transfer Object)来传数据。你想给每个类实现一个有意义的 ToString,列出所有属性。
不用手工抄一遍,可以写个 Source Generator,对于带有 [AutoToString] 特性的每个类生成 ToString 的实现。比如有库 AutoToString 就这么做的。
// 你的类,带特性
[AutoToString]
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
// Source Generator 会生成(简化版):
public partial class Person
{
public override string ToString() => $"Person: Name={Name}, Age={Age}";
}
优点:代码可读且在添加新属性后会自动更新。
简化序列化和反序列化
Source Generators 在标准库里也有广泛使用,比如 System.Text.Json,用来生成高性能的序列化/反序列化代码。以前通常得靠反射(性能贵),现在生成的源码让一切既快又安全。
开发者得到什么? 你标注 [JsonSerializable(typeof(MyType))] —— 生成器就会为这个类型创建高性能的序列化代码。
为配置、映射、DI 容器生成代码
- 配置:生成器可以根据 JSON 文件自动生成配置类。
- 映射器:比如 Mapster 用生成器来创建类型之间的映射,免去手工拷贝字段。
- Dependency Injection:有些容器(比如 StrongInject)用生成器来生成服务注册的代码。
与外部基础设施集成
有些生成器会分析外部资源(API 描述、GraphQL schema、Thrift 协议等)并生成相应的 C# 类,这样合同变化时就不用手工去改代码。
在编译阶段做校验和诊断
Source Generators 还能用来做代码检查:“如果项目里出现方法 X,但没有满足条件 Y —— 在编译器里给警告!”很多 linter 和 analyzer 就这么工作,生成器还能插入提示或直接提供占位实现。
2. 给自动序列化写个 Source Generator
我们来看个很小的“直观”示例:写一个生成器,对于带有 [AutoJson] 特性的类生成一个把对象序列化为 JSON 的方法。
下面的代码只是示意;实际场景建议用 System.Text.Json.SourceGeneration。
// 手写的类:
[AutoJson]
public partial class Book
{
public string Title { get; set; }
public int Year { get; set; }
}
// 生成器会加入:
public partial class Book
{
public string ToJson() => $"{{ \"Title\": \"{Title}\", \"Year\": {Year} }}";
}
Source Generator 和项目的交互流程
flowchart TD
A(你的 Source Code) -->|编译器 调用| B(Source Generator)
B -->|生成的 代码| C(新的 .cs 文件)
C --> D(项目编译)
- 先是编译器(Roslyn)分析你的源码。
- 然后调用你写的生成器(实现了 ISourceGenerator)。
- 生成器添加新的 .cs 文件,这些文件成为编译时的语法树一部分。
- 最后的程序集同时包含你写的代码和生成的代码。
专用任务:还能生成什么
- 本地库绑定(C 代码或 WinAPI 的包装)。
- AOP:自动记录调用、切面(类 Fody、PostSharp 的思路)。
- API 描述 给文档自动生成器用。
- 外部资源处理:SVG、SQL、Razor —— 生成 strongly-typed 类以便类型安全访问。
3. 用 System.Reflection.Emit 做运行时动态生成
如果 Source Generators 在编译前生成 C# 源码,那么 System.Reflection.Emit 就是运行时的魔法。你的程序可以自己创建新类型、方法甚至程序集——就在运行中!
听起来吓人?有点儿。但有些场景不得不用:比如做动态代理的框架(AOP、profiling、mocking)、根据运行时数据生成序列化器、动态 ORM 等。
什么时候需要 Reflection.Emit
- 类型在编译时未知(用户在运行时定义结构)。
- 动态代理(给调用加个拦截器的包装)。
- 高性能序列化(例如 protobuf-net)。
- 插件和脚本引擎,加载场景复杂时用到。
通过 Reflection.Emit 可以创建什么
- AssemblyBuilder — 新的程序集。
- ModuleBuilder — 程序集里模块。
- TypeBuilder — 新类型的描述。
- MethodBuilder — 包含 IL 的方法。
- PropertyBuilder, FieldBuilder, EventBuilder — 属性、字段、事件。
迷你示例:运行时生成一个类
using System;
using System.Reflection;
using System.Reflection.Emit;
public static class DynamicTypeGenerator
{
public static Type GenerateSimpleType(string typeName)
{
// 1. 创建程序集和模块
var assemblyName = new AssemblyName("DynamicAssembly");
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
// 2. 定义一个新类
var typeBuilder = moduleBuilder.DefineType(
typeName,
TypeAttributes.Public | TypeAttributes.Class
);
// 3. 添加一个公有的字符串属性 Title
var field = typeBuilder.DefineField("_title", typeof(string), FieldAttributes.Private);
var prop = typeBuilder.DefineProperty("Title", PropertyAttributes.HasDefault, typeof(string), null);
var getMethod = typeBuilder.DefineMethod("get_Title", MethodAttributes.Public, typeof(string), Type.EmptyTypes);
var il = getMethod.GetILGenerator();
il.Emit(OpCodes.Ldarg_0); // this
il.Emit(OpCodes.Ldfld, field); // _title
il.Emit(OpCodes.Ret);
prop.SetGetMethod(getMethod);
var setMethod = typeBuilder.DefineMethod("set_Title", MethodAttributes.Public, null, new[] { typeof(string) });
il = setMethod.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Stfld, field);
il.Emit(OpCodes.Ret);
prop.SetSetMethod(setMethod);
// 4. 完成!创建 Type
return typeBuilder.CreateTypeInfo();
}
}
现在可以像普通 C# 对象那样使用这个类型,比如通过反射:
var dynamicType = DynamicTypeGenerator.GenerateSimpleType("Book");
var obj = Activator.CreateInstance(dynamicType);
dynamicType.GetProperty("Title").SetValue(obj, "C# 人物");
Console.WriteLine(dynamicType.GetProperty("Title").GetValue(obj)); // C# 人物
这样就能造出 ORM、代理、序列化器、性能分析器,甚至一些测试框架。
4. 有用的细节
Source Generators vs Reflection.Emit:谁适合谁?
Source Generators 在编译阶段工作:让你的源码变聪明,结果成为已编译程序集的一部分。它们不能基于运行时才获得的数据来生成代码。
Reflection.Emit 在运行时工作:允许动态创建程序集、类型和方法,但这类代码更难调试和维护。
什么时候用哪个?
生活中的类比:
- Source Generators — 像工厂,在装配前生产好零件。
- Reflection.Emit — 像工程师,在行驶中给车上焊个火箭发动机。
注意点和坑
- 生成的代码可能不好调试。生成器通常能把源码保存到磁盘——找找 obj\Generated 目录。
- Reflection.Emit 创建的是内存中的程序集,通常不能从 AppDomain 卸载。对临时程序集请使用 AssemblyBuilderAccess.RunAndCollect(如果运行时支持的话)。
- 别滥用 Reflection.Emit 做简单任务 —— 有时候生成源码或普通模板化代码更简单更稳妥。
- 生成器需要懂 Roslyn(用于 Source Generators)和 IL(用于 Reflection.Emit)。
Source Generators 和 Reflection.Emit
| 评估标准 | Source Generators | Reflection.Emit |
|---|---|---|
| 使用时机 | 在编译阶段 | 在运行时 |
| 结果 | C# 源码,成为程序集一部分 | IL 代码,新的类型/程序集 |
| 常见场景 | 自动生成模板代码、DI、映射、序列化 | 代理、动态 ORM、特定的 runtime 流水线 |
| 使用难度 | 中等,需要 Roslyn 知识 | 较高,需要 IL 知识 |
| IDE 和调试支持 | 很好(能看到源码) | 比较困难 |
| 性能 | 非常高 | 可以很高 |
5. 实战场景
1. 生成 strongly-typed API
公司提供 OpenAPI 规范。生成器分析它并创建控制器类、DTO 和客户端代码来调用 REST API —— 类型安全并且支持 IntelliSense。
代码(伪):
// Spec: GET /users -> returns User[]
// Source Generator 会生成:
public class ApiClient
{
public Task<User[]> GetUsersAsync() { ... }
}
2. 自动化注入/DI 容器(编译期 IoC)
生成器可以为服务注册生成 builder,搭建对象图。就不用手动写 services.AddSingleton<IMyService, MyService>() 了。
代码(伪):
[Injectable]
public class MyService : IMyService { ... }
// Source Generator 会生成:
partial class DIContainer
{
public void RegisterServices()
{
AddSingleton<IMyService, MyService>();
}
}
3. 动态代理 — 用 Reflection.Emit 的例子
库 Castle DynamicProxy 构建代理类型,拦截方法调用 —— 是 AOP、日志、追踪、mocking 的基础。
代码(简化):
public interface IBookService { string GetBook(); }
public class BookService : IBookService { public string GetBook() => "C#"; }
var proxy = ProxyGenerator.CreateProxy<IBookService>(new BookService(), interceptor);
proxy.GetBook(); // 调用被拦截,可以记录/修改结果
4. 无反射的高速序列化
与其在运行时用反射拼类型描述(慢),不如用 Source Generators 在编译期生成序列化/反序列化代码 —— 速度更快且没有运行时开销。
GO TO FULL VERSION