跳到主要内容

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

// ジェネレーターのコード
}

よくある問題

  1. 生成されたコードが見つからない

    • OutputItemType="Analyzer" が設定されているか確認
    • ビルドをクリーンしてリビルド
  2. 部分クラス (partial) エラー

    • 生成対象のクラスは partial として宣言する必要があります
  3. パフォーマンスの問題

    • IIncrementalGeneratorを使用する(ISourceGeneratorより効率的)
    • 不要なSyntax Treeの走査を避ける

ベストプラクティス

  1. Incremental Generatorを使用する

    [Generator]
    public class MyGenerator : IIncrementalGenerator
    {
    // ISourceGeneratorより効率的
    }
  2. 診断メッセージを提供する

    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));
  3. 生成ファイル名に.g.csサフィックスを使用

    context.AddSource($"{className}.g.cs", source);
  4. netstandard2.0をターゲットにする

    • 最大の互換性のため
  5. 適切な名前空間を使用

    • 生成されたコードが元のコードと同じ名前空間を使用するようにする

パフォーマンスへの影響

Source Generatorsは、特に以下のシナリオでパフォーマンスを大幅に向上させます:

  • JSONシリアライゼーション: リフレクションベースと比較して最大40%高速化
  • ロギング: 文字列アロケーションを削減し、最大8倍高速化
  • 正規表現: コンパイル時最適化により10-20%高速化
  • ORMマッピング: 動的マッピングと比較して2-3倍高速化

まとめ

Source Generatorsは、.NETにおけるメタプログラミングの新しいスタンダードです。

主な利点:

  • ✅ コンパイル時のコード生成により実行時オーバーヘッドなし
  • ✅ Native AOT対応
  • ✅ 型安全性とコンパイル時検証
  • ✅ デバッグとメンテナンスが容易
  • ✅ リフレクションと比較して大幅なパフォーマンス向上

使用を検討すべき場合:

  • ボイラープレートコードの削減
  • シリアライゼーション/デシリアライゼーション
  • 高性能なロギング
  • パフォーマンスが重要なアプリケーション
  • Native AOTやトリミングを使用する場合

Source Generatorsを適切に使用することで、コードの保守性を保ちながら、パフォーマンスを最適化できます。