1. 簡介
老實說:大多數人對程式碼生成的想像,大概跟切蛋器一樣 — 有用,但很少是必需的。不過現在的 .NET 專案越來越複雜,把例行或模板化的工作自動化,不只是「花招」,而是提升品質和生產力的重要工具。
Source Generators 是從 C# 9 跟 .NET 5 開始有的機制。它們像是在編譯階段的超級英雄,可以生成 C# 原始碼,然後這些原始碼會作為你專案的一部分被編譯。它們不會在執行時改動已編譯的 assembly,而是把新的原始碼在編譯前加入到專案裡。
下面會列出一些 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,用來生成高效的序列化/反序列化程式碼。生成器出現之前,序列化常常要靠 reflection(效能昂貴),現在預生成的程式碼讓速度快又安全。
開發者得到什麼? 你標註 [JsonSerializable(typeof(MyType))] — 生成器就會為該型別生成高效的序列化程式碼。
為設定、mapper、DI-container 生成程式碼
- 設定檔:生成器可以依照 JSON 檔自動生成設定類別。
- Mapper:例如 Mapster 用生成器在型別間建立 mapping,免去手動拷貝欄位。
- Dependency Injection:有些容器(像 StrongInject)用生成器來寫註冊服務的程式碼。
整合外部基礎設施
有些生成器會分析外部資源(API 描述、GraphQL schema、Thrift 協定等),生成對應的 C# 類別來使用它們。這樣在契約變更時就不用手動更新程式碼。
編譯時的檢查與診斷
Source Generators 也能用來檢查程式碼:「如果專案中出現方法 X,但沒滿足條件 Y — 在編譯器給出警告!」很多 linter 和 analyzer 就是這樣工作的;生成器也可以加自訂訊息,甚至塞入一段預設的 stub 程式碼。
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(你的原始碼) -->|編譯器呼叫| B(Source Generator)
B -->|新增的 Code| C(新的 .cs 檔案)
C --> D(專案編譯)
- 首先編譯器(Roslyn)會分析你的原始碼。
- 然後呼叫「你的」生成器(實作了 ISourceGenerator)。
- 生成器新增 .cs 檔案,這些檔案會成為整個編譯樹的一部分。
- 最終組建包含你寫的程式碼和生成的程式碼。
專門任務:還可以生成些什麼
- Native binding 的綁定層(C code 或 WinAPI 的 wrapper)。
- AOP:自動記錄呼叫、aspect(像 Fody、PostSharp 等的思路)。
- API 文件描述 給自動生成文件的工具使用。
- 處理外部資源:SVG、SQL、Razor — 生成器產出 strongly-typed 類別以利型別安全的存取。
3. 用 System.Reflection.Emit 做動態生成程式碼
如果 Source Generators 在編譯前生成 C# 原始碼,那 System.Reflection.Emit 就是用來做執行期的魔法。你的程式可以在執行時自己建立新的型別、方法、甚至 assembly — 就是動態產生東西!
聽起來有點嚇人?有一點。但有時候別無選擇:像是你寫動態 proxy 的框架(AOP、profiling、mocking)、根據 runtime 資料生成的序列化器、動態 ORM 等等,就會用到它。
什麼時候需要 Reflection.Emit
- 型別無法在事先知道(使用者在執行時定義結構)。
- 動態 proxy(攔截呼叫的 wrapper)。
- 高效能序列化(例如 protobuf-net)。
- 複雜載入情境的 plugin 或 script engine。
透過 Reflection.Emit 可以建立的東西
- AssemblyBuilder — 建立新的 assembly。
- ModuleBuilder — assembly 裡的模組。
- 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. 建立 assembly 和 module
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# 物件一樣使用這個型別,例如用 reflection:
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、proxy、序列化器、profiler 以及某些測試框架誕生的方式。
4. 好用的小技巧
Source Generators vs Reflection.Emit:誰適合誰?
Source Generators 在編譯階段運作:讓你的原始碼更聰明,結果會進到已編譯的 assembly。它們不能根據在程式執行時才取得的資料來生成程式碼。
Reflection.Emit 在 runtime 運作:可以動態建立 assembly、型別和方法,但這類程式碼比較難除錯和維護。
什麼時候用哪一個?
生活化比喻:
- Source Generators — 像工廠,在裝車前就生好零件。
- Reflection.Emit — 像工程師在行駛中即時往車上焊接火箭引擎。
注意事項與坑
- 生成的程式碼可能不好除錯。生成器通常可以把原始碼存到磁碟 — 找找看 obj\Generated 資料夾。
- Reflection.Emit 建立的 assembly 通常在記憶體中,且不會被從 AppDomain 卸載。若是暫時性的 assembly,使用 AssemblyBuilderAccess.RunAndCollect(若支援的話)。
- 別為簡單任務濫用 Reflection.Emit — 有時用 Source Generator 或簡單的程式模板更簡單可靠。
- 生成器需要懂 Roslyn(對於 Source Generators)或 IL(對於 Reflection.Emit)。
Source Generators 和 Reflection.Emit
| 準則 | Source Generators | Reflection.Emit |
|---|---|---|
| 使用階段 | 在編譯階段 | 在 runtime |
| 結果 | C# 原始碼,成為組建的一部分 | IL 程式碼,新的型別/assembly |
| 常見場景 | 自動生成樣板程式碼、DI、mapping、序列化 | proxy、動態 ORM、特殊的 runtime pipeline |
| 使用難度 | 中等,需要 Roslyn 知識 | 高,需要 IL 知識 |
| IDE 與除錯支援 | 很好(可以看到原始檔) | 比較困難 |
| 效能 | 非常高 | 可能很高 |
5. 實務場景
1. 生成 strongly-typed API
公司提供 OpenAPI 規格。生成器解析它並產生 controller 類別、DTO 和客戶端程式碼來安全地與 REST API 互動 — 支援 IntelliSense。
程式碼(偽):
// Spec: GET /users -> returns User[]
// Source Generator 會生成:
public class ApiClient
{
public Task<User[]> GetUsersAsync() { ... }
}
2. 自動化 injection/DI-container(Compile-time IoC)
生成器可以為依賴註冊與物件圖建構生成 builder。你就不用手動寫 services.AddSingleton<IMyService, MyService>() 這類的註冊程式碼。
程式碼(偽):
[Injectable]
public class MyService : IMyService { ... }
// Source Generator 會生成:
partial class DIContainer
{
public void RegisterServices()
{
AddSingleton<IMyService, MyService>();
}
}
3. 動態 proxy — Reflection.Emit 範例
像 Castle DynamicProxy 這類庫會建立 proxy 型別來攔截方法呼叫 — 這是 AOP、記錄、tracing、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. 快速序列化,避免 reflection
與其在 runtime 用 reflection 去構建型別描述(慢),Source Generators 可以事先生成序列化/反序列化程式碼 — 速度最快且沒額外開銷。
GO TO FULL VERSION