C# IncrementalGenerator(SourceGenerator)の初歩的な使い方
はじめに
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)とそろえてください
- 後述するNugetパッケージ
-
個別コンポーネント(.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
インターフェイスを実装します。
以下は基本的なテンプレートコードです。
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
を生成します。
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!";
}
""");
});
}
}
生成されたクラス:
// <auto-generated />
namespace GeneratorSample;
public class HelloWorld
{
public string Message => "Hello, World!";
}
3.2. JSONファイルを元にクラスファイルを生成する
ここでは、JSONファイルを元に動的にクラスを生成します。以下のJSONを元にPerson
クラスを生成します。
このときParserライブラリを使う場合、そのライブラリが依存するNugetパッケージをcsprojにすべて記述します
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ファイル
{
"ClassName": "Person",
"Properties": [
{ "Name": "Id", "Type": "int" },
{ "Name": "Name", "Type": "string" }
]
}
コード:
[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; }
}
}
生成されたクラス:
// <auto-generated />
namespace GeneratorSample;
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
3.3. 実装されたクラスに特定の属性があればPartialクラスを作成する
以下では、SyntaxProvider.ForAttributeWithMetadataName
によって特定の属性を持つクラスから、Emit
メソッドを通じてコード生成を行います。
コード:
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);
}
}
生成されたクラス:
// <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でGeneratorを使用するプロジェクト(ConsoleApp)の
-
Visual Studioでのデバッグ
- Generatorプロジェクトをデバッグ対象に設定。
- デバッグプロファイルで、
Roslyn Component
のものを新規作成
対象プロジェクトにConsoleApp(Generaorを参照しているプロジェクト)を選択 - コード生成の過程をデバッグできる
※neue.ccさんのドキュメントのほうが数倍わかりやすいです
5. まとめ
- Incremental Generatorは、柔軟かつ効率的にコードを生成できます。
- 本記事で紹介した3つの使い方のうち、AdditionalFiles(アナライザー追加ファイル)でJSONだけでなくCSVから生成したりと応用の幅は広そうです。
Discussion