1. はじめに
正直に言うと:多くの人はコード生成を卵スライサーみたいに考えています — 便利だけど滅多に必須ではない。けれど現代の .NET プロジェクトはどんどん複雑になっていて、定型作業やテンプレート的なコードの自動化は「飾り」ではなく品質と生産性を上げる重要なツールです。
Source Generators は C# 9 と .NET 5 から登場した仕組みです。コンパイル時のスーパーヒーローみたいに、プロジェクトの一部としてコンパイルされる C# コードを生成できます。実行時に既にコンパイルされたアセンブリのコードを変更するのではなく、コンパイル前に新しいソースをプロジェクトに追加します。
以下に Source Generators の典型的な使用シナリオを挙げます。
テンプレート的なコードの自動生成
多くのプロジェクトでは同じようなコードを作る必要があります:コンストラクタ、ToString メソッド、シリアライザ、プロパティ変更通知(INotifyPropertyChanged)など。Source Generators はこうしたコードを自動生成して、開発者をルーチン作業やタイプミスのリスクから解放します。
例:ToString ジェネレータ
例えば DTO クラス(Data Transfer Object)を作っているとします。各クラスに意味のある ToString を実装して、全プロパティを列挙したいとします。
手でコピペする代わりに、[AutoToString] 属性が付いた各クラスに対して ToString を生成する Source Generator を作れます。例えばライブラリ 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 スキーマ、Thrift など)を解析して、それらとやり取りするための C# クラスを生成します。これにより契約が変わっても手動でコードを更新する手間が減ります。
コンパイル時の検査と診断
Source Generators はコード検査にも使えます:「プロジェクトにメソッド X があるが条件 Y が満たされていない場合、コンパイラに警告を出す」など。多くのリンタや 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(あなたのソースコード) -->|コンパイラが呼び出す| 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、プロファイリング、モッキング)、ランタイムデータに基づくシリアライザ、動的 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. 公開の string プロパティ 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 の理解が、Reflection.Emit には IL の理解が必要です。
Source Generators と Reflection.Emit
| 基準 | Source Generators | Reflection.Emit |
|---|---|---|
| 使われるタイミング | コンパイル時 | ランタイム |
| 結果 | C# ソース、ビルドの一部 | IL コード、新しい型/アセンブリ |
| 典型的なシナリオ | テンプレートコードの自動生成、DI、マッピング、シリアライゼーション | プロキシ、動的 ORM、特殊なランタイムパイプライン |
| 利用の難易度 | 中程度、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. 自動的な injection/DI コンテナ(Compile-time IoC)
ジェネレータは依存関係登録用のビルダを作り、オブジェクトグラフの構築コードを生成します。手で 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、ロギング、トレース、モッキングの基盤です。
コード(簡略):
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