Source Generators
概要
Source Generators(ソースジェネレーター)は、.NET 5から導入されたコンパイル時にC#コードを生成する機能です。コンパイラがソースコードを解析する際に実行され、新しいソースファイルをコンパイルプロセスに追加します。これにより、実行時のリフレクションに依存せずにメタプログラミングを実現できます。
Source Generatorsとは?
Source Generatorsは、コンパイル時にRoslynコンパイラによって実行される特殊な.NETアナライザーです。
主な特徴
- コンパイル時実行: ビルド時にコードを生成するため、実行時のオーバーヘッドがありません
- パフォーマンス: リフレクションと比較して実行時のパフォーマンスが大幅に向上します
- AOT対応: Native AOT(Ahead-of-Time)コンパイルに対応しており、トリミング環境でも動作します
- 型安全性: 生成されたコードはコンパイル時に検証されます
リフレクションとの比較
| 特徴 | Source Generators | リフレクション |
|---|---|---|
| 実行タイミング | コンパイル時 | 実行時 |
| パフォーマンス | 高速(オーバーヘッドなし) | 低速(動的解決が必要) |
| AOT対応 | ✅ 完全対応 | ❌ 制限あり |
| デバッグ | 生成コードを直接確認可能 | 実行時のみ確認可能 |
| 柔軟性 | コンパイル時の情報のみ | 実行時の状態にアクセス可能 |
いつSource Generatorsを使うべきか
Source Generatorsが適している場合:
- ボイラープレートコードの自動生成
- シリアライゼーション/デシリアライゼーション
- ロギングコードの生成
- パフォーマンスクリティカルな処理
リフレクションが適している場合:
- 実行時にのみ決定される型の処理
- プラグインシステムやDLL動的読み込み
- 設定やデータ駆動の動的な振る舞い
Source Generatorの作成
プロジェクトのセットアップ
まず、Source Generator専用のクラスライブラリプロジェクトを作成します。
dotnet new classlib -n MySourceGenerator
cd MySourceGenerator
プロジェクトファイル(.csproj)に必要なパッケージを追加します:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" PrivateAssets="all" />
</ItemGroup>
</Project>
基本的なSource Generatorの実装
シンプルなHello Worldジェネレーターを作成します:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
namespace MySourceGenerator;
[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
// 初期化処理(必要に応じて)
}
public void Execute(GeneratorExecutionContext context)
{
// 生成するコード
var sourceBuilder = new StringBuilder(@"
namespace GeneratedCode
{
public static class HelloWorld
{
public static string SayHello() => ""Hello from Source Generator!"";
}
}
");
// ソースを追加
context.AddSource("HelloWorld.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
}
Source Generatorの使用
生成されたコードを使用するプロジェクトの .csproj ファイル:
<ItemGroup>
<ProjectReference Include="..\MySourceGenerator\MySourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
使用例:
using GeneratedCode;
class Program
{
static void Main()
{
Console.WriteLine(HelloWorld.SayHello());
// 出力: Hello from Source Generator!
}
}
実践的な例:属性ベースのコード生成
より実用的な例として、属性を使用してToStringメソッドを自動生成するジェネレーターを作成します。
マーカー属性の定義
using System;
namespace SourceGeneratorAttributes;
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
public sealed class GenerateToStringAttribute : Attribute
{
}
Incremental Source Generatorの実装
.NET 6以降では、IIncrementalGeneratorの使用が推奨されます:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
namespace MySourceGenerator;
[Generator]
public class ToStringGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 属性が付いたクラスをフィルタリング
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => node is ClassDeclarationSyntax,
transform: static (ctx, _) => GetSemanticTargetForGeneration(ctx))
.Where(static m => m is not null);
// 生成を実行
context.RegisterSourceOutput(classDeclarations,
static (spc, source) => Execute(source!, spc));
}
private static ClassDeclarationSyntax? GetSemanticTargetForGeneration(
GeneratorSyntaxContext context)
{
var classDeclaration = (ClassDeclarationSyntax)context.Node;
// GenerateToString属性があるかチェック
foreach (var attributeList in classDeclaration.AttributeLists)
{
foreach (var attribute in attributeList.Attributes)
{
var symbol = context.SemanticModel.GetSymbolInfo(attribute).Symbol;
if (symbol?.ContainingType?.Name == "GenerateToStringAttribute")
{
return classDeclaration;
}
}
}
return null;
}
private static void Execute(ClassDeclarationSyntax classDeclaration,
SourceProductionContext context)
{
var className = classDeclaration.Identifier.Text;
var namespaceName = GetNamespace(classDeclaration);
var source = GenerateToStringMethod(namespaceName, className);
context.AddSource($"{className}.g.cs", source);
}
private static string GetNamespace(ClassDeclarationSyntax classDeclaration)
{
var namespaceDeclaration = classDeclaration.Parent as NamespaceDeclarationSyntax;
return namespaceDeclaration?.Name.ToString() ?? "Global";
}
private static string GenerateToStringMethod(string namespaceName, string className)
{
return $@"
namespace {namespaceName}
{{
partial class {className}
{{
public override string ToString()
{{
var properties = GetType().GetProperties()
.Select(p => $""{{p.Name}}={{p.GetValue(this)}}"")
.ToArray();
return $""{className} {{ {{string.Join("", "", properties)}} }}"";
}}
}}
}}
";
}
}
使用例
using SourceGeneratorAttributes;
namespace MyApp;
[GenerateToString]
public partial class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
// 使用
var person = new Person { Name = "Alice", Age = 30 };
Console.WriteLine(person.ToString());
// 出力: Person { Name=Alice, Age=30 }
.NETにおける実用例
System.Text.Json Source Generator
ASP.NET Core 6以降では、JSONシリアライゼーションにSource Generatorを使用できます:
using System.Text.Json.Serialization;
[JsonSerializable(typeof(Person))]
[JsonSerializable(typeof(List<Person>))]
internal partial class MyJsonContext : JsonSerializerContext
{
}
// 使用
var options = new JsonSerializerOptions
{
TypeInfoResolver = MyJsonContext.Default
};
var json = JsonSerializer.Serialize(person, options);
メリット:
- 実行時リフレクション不要
- AOT対応
- パフォーマンス向上(最大40%高速化)
LoggerMessage Source Generator
高性能なロギングメソッドの生成:
public static partial class LoggerExtensions
{
[LoggerMessage(
EventId = 0,
Level = LogLevel.Information,
Message = "Processing request for {UserName} at {Timestamp}")]
public static partial void LogProcessingRequest(
this ILogger logger, string userName, DateTime timestamp);
}
// 使用
logger.LogProcessingRequest("Alice", DateTime.UtcNow);
メリット:
- アロケーション削減
- 文字列補間のオーバーヘッド削減
- 型安全なロギング
Regex Source Generator
正規表現のコンパイル時最適化:
public partial class RegexPatterns
{
[GeneratedRegex(@"^\d{3}-\d{4}$")]
public static partial Regex PhoneNumberPattern();
}
// 使用
if (RegexPatterns.PhoneNumberPattern().IsMatch(input))
{
// マッチした場合の処理
}
デバッグとトラブルシューティング
生成されたコードの確認
生成されたコードは、プロジェクトの obj フォルダ内に保存されます:
obj/Debug/net8.0/generated/MySourceGenerator/MySourceGenerator.ToStringGenerator/
Visual Studioでは、ソリューションエクスプローラーで「Dependencies > Analyzers」配下から確認できます。
デバッグ方法
Source Generatorをデバッグするには、.csprojに以下を追加します:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Generated/**/*.cs" />
</ItemGroup>
デバッガーをアタッチする方法:
public void Execute(GeneratorExecutionContext context)
{
#if DEBUG
if (!System.Diagnostics.Debugger.IsAttached)
{
System.Diagnostics.Debugger.Launch();
}
#endif
// ジェネレーターのコード
}
よくある問題
-
生成されたコードが見つからない
OutputItemType="Analyzer"が設定されているか確認- ビルドをクリーンしてリビルド
-
部分クラス (partial) エラー
- 生成対象のクラスは
partialとして宣言する必要があります
- 生成対象のクラスは
-
パフォーマンスの問題
IIncrementalGeneratorを使用する(ISourceGeneratorより効率的)- 不要なSyntax Treeの走査を避ける
ベストプラクティス
-
Incremental Generatorを使用する
[Generator]public class MyGenerator : IIncrementalGenerator{// ISourceGeneratorより効率的} -
診断メッセージを提供する
context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor(id: "MYGEN001",title: "Invalid usage",messageFormat: "Class {0} must be partial",category: "MyGenerator",DiagnosticSeverity.Error,isEnabledByDefault: true),classDeclaration.GetLocation(),className)); -
生成ファイル名に.g.csサフィックスを使用
context.AddSource($"{className}.g.cs", source); -
netstandard2.0をターゲットにする
- 最大の互換性のため
-
適切な名前空間を使用
- 生成されたコードが元のコードと同じ名前空間を使用するようにする
パフォーマンスへの影響
Source Generatorsは、特に以下のシナリオでパフォーマンスを大幅に向上させます:
- JSONシリアライゼーション: リフレクションベースと比較して最大40%高速化
- ロギング: 文字列アロケーションを削減し、最大8倍高速化
- 正規表現: コンパイル時最適化により10-20%高速化
- ORMマッピング: 動的マッピングと比較して2-3倍高速化
まとめ
Source Generatorsは、.NETにおけるメタプログラミングの新しいスタンダードです。
主な利点:
- ✅ コンパイル時のコード生成により実行時オーバーヘッドなし
- ✅ Native AOT対応
- ✅ 型安全性とコンパイル時検証
- ✅ デバッグとメンテナンスが容易
- ✅ リフレクションと比較して大幅なパフォーマンス向上
使用を検討すべき場合:
- ボイラープレートコードの削減
- シリアライゼーション/デシリアライゼーション
- 高性能なロギング
- パフォーマンスが重要なアプリケーション
- Native AOTやトリミングを使用する場合
Source Generatorsを適切に使用することで、コードの保守性を保ちながら、パフォーマンスを最適化できます。