ソース ジェネレーター

この記事では、.NET Compiler Platform ("Roslyn") SDK に付属するソース ジェネレーターの概要を説明します。 ソース ジェネレーターを使用すると、C# の開発者がコンパイル時にユーザー コードを検査できます。 ソース ジェネレーターは、ユーザーのコンパイルに追加される新しい C# のソース ファイルを即座に作成します。 この方法で、コードをコンパイル中に実行することができます。 プログラムが検査され、コードの残りの部分と共にコンパイルされる追加のソース ファイルが生成されます。

ソース ジェネレーターは、C# 開発者が作成できる新しい種類のコンポーネントであり、次の 2 つの主要な作業を実行できます。

  1. コンパイルされているすべてのユーザー コードを表す "コンパイル オブジェクト" を取得します。 このオブジェクトを検査することができ、現在のアナライザーと同様に、コンパイルされているコードの構文およびセマンティック モデルで動作するコードを記述できます。

  2. コンパイル中にコンパイル オブジェクトに追加できる C# のソース ファイルを生成します。 つまり、コードがコンパイルされている間に、コンパイルへの入力として追加のソース コードを提供できます。

これら 2 つの機能が組み合わされていることで、ソース ジェネレーターは役に立ちます。 ユーザー コードの検査では、コンパイル中にコンパイラで構築されるリッチなメタデータをすべて使用できます。 その後、ジェネレーターは、分析したデータに基づいて同じコンパイルに C# コードを出力します。 Roslyn アナライザーを使い慣れている場合は、ソース ジェネレーターを、C# ソース コードを出力できるアナライザーと考えることができます。

ソース ジェネレーターは、次に示すコンパイルのフェーズとして実行されます。

Graphic describing the different parts of source generation

ソース ジェネレーターは、コンパイラによってアナライザーと共に読み込まれる .NET Standard 2.0 アセンブリです。 .NET Standard コンポーネントを読み込んで実行できる環境で使用できます。

重要

現時点で、ソース ジェネレーターとして使用できるのは .NET Standard 2.0 アセンブリのみです。

一般的なシナリオ

現在のテクノロジで使用されている分析に基づいて、ユーザーコードを検査したり、情報やコードを生成したりするには、一般的に次の 3 つの方法があります。

  • ランタイム リフレクション。
  • MSBuild タスクのジャグリング。
  • 中間言語 (IL) ウィービング (この記事では説明しません)。

ソース ジェネレーターは、各アプローチを改善することができます。

ランタイム リフレクション

ランタイム リフレクションは、以前に .NET に追加された強力なテクノロジです。 これを使用するには、さまざまなシナリオがあります。 一般的なシナリオは、アプリの起動時にユーザー コードの分析を実行し、そのデータを使用して何かを生成することです。

たとえば、ASP.NET Core では、Web サービスが最初に実行されるときにリフレクションを使用して、定義されているコンストラクトが検出され、コントローラーや Razor ページなどを "接続" できるようにします。 これにより、強力な抽象化を使用して簡単なコードを記述することができますが、実行時のパフォーマンスが低下します。Web サービスまたはアプリの初期起動時には、コードに関する情報を検出するすべてのランタイム リフレクション コードの実行が完了するまで、要求を受け入れられません。 このパフォーマンスの低下は大きなものではありませんが、独自のアプリにおいて自力では改善できないある程度の固定コストがかかります。

ソース ジェネレーターでは起動時のコントローラー検出フェーズが代わりにコンパイル時に発生する場合があります。 ジェネレーターは、ソース コードを分析し、アプリを "接続" するために必要なコードを出力することができます。 ソース ジェネレーターを使用することにより、現在実行時に行われているアクションをコンパイル時に移動できるため、起動時間が短縮される可能性があります。

MSBuild タスクのジャグリング

ソース ジェネレーターを使用すると、型を検出するための実行時のリフレクションに限定されない方法で、パフォーマンスを向上させることができます。 いくつかのシナリオでは、コンパイルのデータを検査できるように、MSBuild C# タスク (CSC と呼ばれます) が複数回呼び出されます。 ご想像のとおり、コンパイラを複数回呼び出すと、アプリのビルドにかかる合計時間に影響します。 ソース ジェネレーターを使用するとパフォーマンスが向上するだけでなく、適切な抽象化レベルでツールを操作できるため、ソース ジェネレーターを使用して、このような MSBuild タスクのジャグリングを不要にする方法が調査されています。

ソース ジェネレーターが提供できるもう 1 つの機能は、コントローラーと Razor ページ間の ASP.NET Core のルーティングなど、一部の "文字列で型指定された" API を使用する必要がないようにすることです。 ソース ジェネレーターを使用すると、コンパイル時の詳細として生成される必要な文字列で、ルーティングを厳密に型指定できます。 これにより、誤入力された文字列リテラルのために要求が正しいコントローラーに到達できない回数を減らすことができます。

ソース ジェネレーターを使い始める

このガイドでは、ISourceGenerator API を使用してソース ジェネレーターを作成する方法について説明します。

  1. .NET コンソール アプリケーションを作成します。 この例では .NET 6 が使用されています。

  2. Program クラスを次のコードで置き換えます。 次のコードは、最上位レベルのステートメントを使用していません。 この最初のソース ジェネレーターでは、その Program クラスに部分メソッドが書き込まれるため、従来の形式が必要です。

    namespace ConsoleApp;
    
    partial class Program
    {
        static void Main(string[] args)
        {
            HelloFrom("Generated Code");
        }
    
        static partial void HelloFrom(string name);
    }
    

    Note

    このサンプルはそのまま実行できますが、まだ何も行われません。

  3. 次に、partial void HelloFrom メソッドと対応するものを実装するソース ジェネレーター プロジェクトを作成します。

  4. netstandard2.0 ターゲット フレームワーク モニカー (TFM) をターゲットとする .NET 標準ライブラリ プロジェクトを作成します。 NuGet パッケージ Microsoft.CodeAnalysis.AnalyzersMicrosoft.CodeAnalysis.CSharp を追加します。

    <Project Sdk="Microsoft.NET.Sdk">
    
      <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
      </PropertyGroup>
    
      <ItemGroup>
        <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" PrivateAssets="all" />
        <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
          <PrivateAssets>all</PrivateAssets>
          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
        </PackageReference>
      </ItemGroup>
    
    </Project>
    

    ヒント

    ソース ジェネレーター プロジェクトは netstandard2.0 TFM をターゲットにする必要があり、それ以外の場合は機能しません。

  5. 次のような独自のソース ジェネレーターを指定する、HelloSourceGenerator.cs という名前の新しい C# ファイルを作成します。

    using Microsoft.CodeAnalysis;
    
    namespace SourceGenerator
    {
        [Generator]
        public class HelloSourceGenerator : ISourceGenerator
        {
            public void Execute(GeneratorExecutionContext context)
            {
                // Code generation goes here
            }
    
            public void Initialize(GeneratorInitializationContext context)
            {
                // No initialization required for this one
            }
        }
    }
    

    ソース ジェネレーターは、Microsoft.CodeAnalysis.ISourceGenerator インターフェイスを実装し、Microsoft.CodeAnalysis.GeneratorAttribute を持つ必要があります。 すべてのソース ジェネレーターで初期化が必要になるわけではありません。この実装例では、ISourceGenerator.Initialize が空の場合に当てはまります。

  6. ISourceGenerator.Execute メソッドの内容を次の実装に置き換えます。

    using Microsoft.CodeAnalysis;
    
    namespace SourceGenerator
    {
        [Generator]
        public class HelloSourceGenerator : ISourceGenerator
        {
            public void Execute(GeneratorExecutionContext context)
            {
                // Find the main method
                var mainMethod = context.Compilation.GetEntryPoint(context.CancellationToken);
    
                // Build up the source code
                string source = $@"// <auto-generated/>
    using System;
    
    namespace {mainMethod.ContainingNamespace.ToDisplayString()}
    {{
        public static partial class {mainMethod.ContainingType.Name}
        {{
            static partial void HelloFrom(string name) =>
                Console.WriteLine($""Generator says: Hi from '{{name}}'"");
        }}
    }}
    ";
                var typeName = mainMethod.ContainingType.Name;
    
                // Add the source code to the compilation
                context.AddSource($"{typeName}.g.cs", source);
            }
    
            public void Initialize(GeneratorInitializationContext context)
            {
                // No initialization required for this one
            }
        }
    }
    

    context オブジェクトから、コンパイルのエントリ ポイントまたは Main メソッドにアクセスできます。 mainMethod インスタンスは IMethodSymbol であり、メソッドまたはメソッドに似たシンボル (コンストラクター、デストラクター、演算子、プロパティやイベント アクセサーを含む) を表します。 Microsoft.CodeAnalysis.Compilation.GetEntryPoint メソッドは、プログラムのエントリ ポイントで IMethodSymbol を返します。 他のメソッドを使用すると、プロジェクト内のすべてのメソッド シンボルを検索できます。 このオブジェクトから、含まれている名前空間 (存在する場合) と型を使用することをお勧めします。 この例の source は、生成されるソース コードをテンプレートに含む補間された文字列です。補間されたホールに、含まれる名前空間と型の情報が挿入されます。 source がヒント名と共に context に追加されます。 この例では、ジェネレーターは、コンソール アプリケーションに partial メソッドの実装を含む新しく生成されたソース ファイルを作成します。 ソース ジェネレーターを記述して、必要なソースを追加できます。

    ヒント

    GeneratorExecutionContext.AddSource メソッドの hintName パラメーターには、任意の一意の名前を指定できます。 名前には、".g.cs"".generated.cs" などの明示的な C# ファイル拡張子を指定するのが一般的です。 ファイル名は、ソースが生成されているファイルを識別するのに役立ちます。

  7. 機能するジェネレーターができましたが、コンソール アプリケーションに接続する必要があります。 元のコンソール アプリケーション プロジェクトを編集して、以下を追加し、プロジェクトのパスを前に作成した .NET Standard プロジェクトのパスに置き換えます。

    <!-- Add this as a new ItemGroup, replacing paths and names appropriately -->
    <ItemGroup>
        <ProjectReference Include="..\PathTo\SourceGenerator.csproj"
                          OutputItemType="Analyzer"
                          ReferenceOutputAssembly="false" />
    </ItemGroup>
    

    この新しい参照は従来のプロジェクト参照ではなく、OutputItemTypeReferenceOutputAssembly の属性を含めるように手動で編集する必要があります。 ProjectReferenceOutputItemTypeReferenceOutputAssembly の属性の詳細については、「MSBuild プロジェクトの共通項目: ProjectReference」を参照してください。

  8. コンソール アプリケーションを実行すると、生成されたコードが実行され、画面に出力されることがわかります。 コンソール アプリケーション自体は HelloFrom メソッドを実装しません。代わりに、ソース ジェネレーター プロジェクトからのコンパイル時にソースが生成されます。 次のテキストは、このアプリケーションからの出力例です。

    Generator says: Hi from 'Generated Code'
    

    Note

    Visual Studio を再起動して IntelliSense を表示し、ツール エクスペリエンスがアクティブに改善されている間にエラーを取り除く必要があります。

  9. Visual Studio を使用している場合は、ソースで生成されたファイルを確認できます。 [ソリューション エクスプローラー] ウィンドウで、[Dependencies]>[Analyzers]>[SourceGenerator]>[SourceGenerator.HelloSourceGenerator] を展開し、Program.g.cs ファイルをダブルクリックします。

    Visual Studio: Solution Explorer source generated files.

    この生成されたファイルを開くと、ファイルが自動生成されたもので、編集できないことが Visual Studio に示されます。

    Visual Studio: Auto-generated Program.g.cs file.

  10. また、ビルド プロパティを設定して、生成されたファイルを保存し、生成されたファイルが格納される場所を制御することもできます。 コンソール アプリケーションのプロジェクト ファイルで、<EmitCompilerGeneratedFiles> 要素を <PropertyGroup> に追加し、その値を true に設定します。 プロジェクトをもう一度ビルドします。 これで、生成されたファイルが obj/Debug/net6.0/generated/SourceGenerator/SourceGenerator.HelloSourceGenerator の下に作成されます。 パスのコンポーネントは、ビルド構成、ターゲット フレームワーク、ソース ジェネレーター プロジェクト名、およびジェネレーターの完全修飾型名にマップされます。 アプリケーションのプロジェクト ファイルに <CompilerGeneratedFilesOutputPath> 要素を追加することで、より便利な出力フォルダーを選択できます。

次のステップ

ソース ジェネレーター クックブックでは、これらの例のいくつかについて、推奨される解決方法が示されています。 また、GitHub に用意されている一連のサンプルを自分で試してみることができます。

ソース ジェネレーターの詳細については、次の記事を参照してください。