🤖

C# IncrementalGenerator(SourceGenerator)の初歩的な使い方

2025/01/27に公開

はじめに

Source Generatorは、C#のコード生成機能の一部で、コンパイル時に自動的にコードを生成できる仕組みです。
本記事では、Incremental Generatorを使った初歩的な解説を行います。

neue.ccさんの2022年(2024年)のC# Incremental Source Generator開発手法を多分に参考にさせてもらいました。この場を借りてお礼申し上げます。

IncrementalGenerator(Source Generator)とは?

Incremental Generatorは、従来のSource Generatorを改良した仕組みで、以下の特徴があります。

  • インクリメンタルなビルド:変更がない部分は再生成されず、効率的にコードが生成される。
  • 分割処理の容易さ:入力データやコード生成ロジックを小さな単位で分割し、管理しやすい。

1. 必要な環境

Incremental Generatorを使うためには以下が必要です。

  • Visual Studio 2022(17.12)以降

    • 後述するNugetパッケージMicrosoft.CodeAnalysisのバージョン(4.12)とそろえてください
  • 個別コンポーネント(.NET Compiler Platform SDK)のインストール

    • Visual Studio Installerからインストールします
  • Generaor用プロジェクト設定
    csprojファイルを次の内容にします。

    GeneratorSample.csproj
    <Project Sdk="Microsoft.NET.Sdk">
        <PropertyGroup>
            <!-- フレームワークは固定 -->
            <TargetFramework>netstandard2.0</TargetFramework>
            <!-- netstandard2.0のC#の既定のバージョンが7.3なので、明示的に上げておく -->
            <LangVersion>12</LangVersion>
    
            <!-- Generatorである宣言 -->
            <IsRoslynComponent>true</IsRoslynComponent>
            <AnalyzerLanguage>cs</AnalyzerLanguage>
            <!-- 警告を消すためのフラグ -->
            <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
    
        </PropertyGroup>
    
        <ItemGroup>
            <!-- Generator用パッケージ -->
            <PackageReference Include="Microsoft.CodeAnalysis" Version="4.12.0" />
        </ItemGroup>
    </Project>
    
  • 参照用プロジェクト設定
    Generatorを使用するプロジェクトのcsprojを次のように書きます

    ConsoleApp.csproj
    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <OutputType>Exe</OutputType>
        <!-- 生成された結果を見るためのフラグ(不要ならfalseで可) -->
        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
      </PropertyGroup>
    
      <ItemGroup>
        <ProjectReference Include="..\GeneratorSample\GeneratorSample.csproj">
          <OutputItemType>Analyzer</OutputItemType>
          <ReferenceOutputAssembly>false</ReferenceOutputAssembly>
        </ProjectReference>
      </ItemGroup>
    </Project>
    

2. Incremental Generatorの基本構造

Incremental Generatorでは、IIncrementalGeneratorインターフェイスを実装します。

以下は基本的なテンプレートコードです。

HelloWorldGenerator.cs
using Microsoft.CodeAnalysis;

namespace GeneratorSample;

[Generator]
public class HelloWorldGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // IncrementalValuesProviderがいくつかあるので、使いたいものを選ぶ
        // 下例では、体裁のために書いているだけの無駄な変数
        var helloWorldProvider = context
            .AdditionalTextsProvider
            .Select((text, _) => string.Empty);

        context.RegisterSourceOutput(helloWorldProvider, Emit);
    }

    private static void Emit(SourceProductionContext context, string _)
    {
        // コード生成
        context.AddSource("HelloWorld.g.cs", """
            // <auto-generated />
            namespace GeneratorSample;

            public class HelloWorld 
            {
                public string Message => "Hello, World!";
            }
            """);
    }
}

3. 具体的な使い方

3.1. 生文字リテラルをそのままクラスファイルにする

以下の例では、生文字リテラルを使ってシンプルなクラスHelloWorldを生成します。

HelloWorldGenerator.cs
using Microsoft.CodeAnalysis;
namespace GeneratorSample;

[Generator(LanguageNames.CSharp)]
public class HelloWorldGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterSourceOutput(context.AdditionalTextsProvider, (ctx, _) =>
        {
            // コード生成
            ctx.AddSource("HelloWorld.g.cs", """
            // <auto-generated />
            namespace GeneratorSample;

            public class HelloWorld 
            {
                public string Message => "Hello, World!";
            }
            """);
        });
    }
}

生成されたクラス:

HelloWorld.g.cs
// <auto-generated />
namespace GeneratorSample;

public class HelloWorld 
{
    public string Message => "Hello, World!";
}

3.2. JSONファイルを元にクラスファイルを生成する

ここでは、JSONファイルを元に動的にクラスを生成します。以下のJSONを元にPersonクラスを生成します。
このときParserライブラリを使う場合、そのライブラリが依存するNugetパッケージをcsprojにすべて記述します

csproj

GeneratorSample.csproj
    <ItemGroup>
        <!-- Generator用パッケージ -->
        <PackageReference Include="Microsoft.CodeAnalysis" Version="4.12.0" />
        <PackageReference Include="System.Text.Json" Version="9.0.0" GeneratePathProperty="true" PrivateAssets="all" />
        <!-- System.Text.Json用-->
        <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.0" GeneratePathProperty="true" PrivateAssets="all" />
        <PackageReference Include="System.Buffers" Version="4.5.1" GeneratePathProperty="true" PrivateAssets="all" />
        <PackageReference Include="System.IO.Pipelines" Version="9.0.0" GeneratePathProperty="true" PrivateAssets="all" />
        <PackageReference Include="System.Memory" Version="4.5.5" GeneratePathProperty="true" PrivateAssets="all" />
        <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.0.0" GeneratePathProperty="true" PrivateAssets="all" />
        <PackageReference Include="System.Text.Encodings.Web" Version="9.0.0" GeneratePathProperty="true" PrivateAssets="all" />
        <PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" GeneratePathProperty="true" PrivateAssets="all" />
    </ItemGroup>
    
    <!-- 依存先の明示 -->
    <PropertyGroup>
        <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
    </PropertyGroup>
    
    <Target Name="GetDependencyTargetPaths">
        <ItemGroup>
            <!-- $(PKGxxx)ではピリオドをアンダーバーに変える -->
            <TargetPathWithTargetPlatformMoniker Include="$(PKGSystem_Text_Json)\lib\netstandard2.0\System.Text.Json.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGMicrosoft_Bcl_AsyncInterfaces)\lib\netstandard2.0\Microsoft.Bcl.AsyncInterfaces.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGSystem_Buffers)\lib\netstandard2.0\System.Buffers.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGSystem_IO_Pipelines)\lib\netstandard2.0\System.IO.Pipelines.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGSystem_Memory)\lib\netstandard2.0\System.Memory.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGSystem_Runtime_CompilerServices_Unsafe)\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGSystem_Text_Encodings_Web)\lib\netstandard2.0\System.Text.Encodings.Web.dll" IncludeRuntimeDependency="false" />
            <TargetPathWithTargetPlatformMoniker Include="$(PKGSystem_Threading_Tasks_Extensions)\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll" IncludeRuntimeDependency="false" />
        </ItemGroup>
    </Target>

JSONファイル

PersonDefine.json
{
  "ClassName": "Person",
  "Properties": [
    { "Name": "Id", "Type": "int" },
    { "Name": "Name", "Type": "string" }
  ]
}

コード:

JsonClassGenerator.cs
[Generator(LanguageNames.CSharp)]
public class JsonClassGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var jsonFiles = context.AdditionalTextsProvider
            .Where(file => file.Path.EndsWith(".json"))
            // .Collect() ファイルが複数あり1つにまとめて生成したい場合は、Collectを使います
            .Select((text, _) =>
            {
                var json = text.GetText()!.ToString();
                var info = System.Text.Json.JsonSerializer.Deserialize<ParsedClassInfo>(json);
                return (Path.GetFileNameWithoutExtension(text.Path), new ParsedClassInfo
                {
                    ClassName = info.ClassName,
                    Properties = info.Properties
                });
            });

        context.RegisterSourceOutput(jsonFiles, Emit);
    }

    private static void Emit(SourceProductionContext context, (string FileName, ParsedClassInfo Info) parsed)
    {
        var builder = new StringBuilder();
        foreach (var p in parsed.Info.Properties)
        {
            builder.AppendLine($$"""
                    public {{p.Type}} {{p.Name}} { get; set; }
                """);
        }

        string source = $$"""
            // <auto-generated />
            namespace GeneratorSample;

            public class {{parsed.Info.ClassName}} 
            {
            {{builder}}
            }
            """;
        context.AddSource($"{parsed.FileName}.g.cs", source);
    }

    private class ParsedClassInfo
    {
        public string ClassName { get; set; }
        public ParsedPropertyInfo[] Properties { get; set; }
    }
    private class ParsedPropertyInfo
    {
        public string Name { get; set; }
        public string Type { get; set; }
    }
}

生成されたクラス:

PersonDefine.g.cs
// <auto-generated />
namespace GeneratorSample;

public class Person 
{
    public int Id { get; set; }
    public string Name { get; set; }

}

3.3. 実装されたクラスに特定の属性があればPartialクラスを作成する

以下では、SyntaxProvider.ForAttributeWithMetadataNameによって特定の属性を持つクラスから、Emitメソッドを通じてコード生成を行います。

コード:

DbRepositoryGenerator.cs
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace GeneratorSample;

[Generator(LanguageNames.CSharp)]
public class DbRepositoryGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput(static context =>
        {
            context.AddSource("DbRepositoryGeneratorAttribute.cs", """
namespace GeneratorSample;

using System;

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class GenerateDbRepositoryAttribute : Attribute
{
}
""");
        });

        var source = context.SyntaxProvider.ForAttributeWithMetadataName(
            "GeneratorSample.GenerateDbRepositoryAttribute",
            static (node, token) => true,
            static (context, token) => context);

        context.RegisterSourceOutput(source, Emit);
    }

    static void Emit(SourceProductionContext context, GeneratorAttributeSyntaxContext source)
    {
        var typeSymbol = (INamedTypeSymbol)source.TargetSymbol;
        var typeNode = (TypeDeclarationSyntax)source.TargetNode;
        var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace
            ? ""
            : $"namespace {typeSymbol.ContainingNamespace};";

        var code = $$"""
// <auto-generated/>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

{{ns}}

public partial class {{typeSymbol.Name}}DbRepository
{
    private readonly AppDbContext _context;
    public {{typeSymbol.Name}}DbRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<{{typeSymbol.Name}}> GetByIdAsync(int id)
    {
        return await _context.{{typeSymbol.Name}}.FindAsync(id);
    }
}
""";

        // AddSourceで出力
        context.AddSource($"{typeSymbol.Name}DbRepository.g.cs", code);
    }
}


生成されたクラス:

SampleEntityDbRepository.g.cs
// <auto-generated/>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace SampleConsleApp;

public partial class SampleEntityDbRepository
{
    private readonly AppDbContext _context;
    public SampleEntityDbRepository(AppDbContext context)
    {
        _context = context;
    }

    public async Task<SampleEntity> GetByIdAsync(int id)
    {
        return await _context.SampleEntity.FindAsync(id);
    }
}

4. デバッグ方法

  • 生成コードの確認
    • Visual StudioでGeneratorを使用するプロジェクト(ConsoleApp)のobj/Debug/net8.0/Generatedフォルダを確認。
    • 出力されていない場合、EmitCompilerGeneratedFilesをtrueにしているか確認
  • Visual Studioでのデバッグ
    1. Generatorプロジェクトをデバッグ対象に設定。
    2. デバッグプロファイルで、Roslyn Componentのものを新規作成
      対象プロジェクトにConsoleApp(Generaorを参照しているプロジェクト)を選択
    3. コード生成の過程をデバッグできる

※neue.ccさんのドキュメントのほうが数倍わかりやすいです

5. まとめ

  • Incremental Generatorは、柔軟かつ効率的にコードを生成できます。
  • 本記事で紹介した3つの使い方のうち、AdditionalFiles(アナライザー追加ファイル)でJSONだけでなくCSVから生成したりと応用の幅は広そうです。

Discussion