TL;DR
- C#に追加されたソース ジェネレーター機能を使って「デザイン時 T4 テキスト テンプレート」を再現してみたかった
- C# ソースファイル(.cs)以外からソースコード生成する方法を解説
- が、
System.CodeDom
絡みのエラーで断念 -
前回と同様の手段で「デザイン時 T4 テキスト テンプレート」を使う手段を紹介
- 方法だけ知りたい人はこちら
環境
- Windows 10 Pro x64
- 下記SDKが使えるならば、Linuxとかでもできるはず(未確認)
- .NET 6 SDK
- C# 9.0に追加されたソース ジェネレーター機能を使いたいので、.NET 5以降が必須です
- Visual Studio Code
- C#拡張機能を入れておきましょう
- T4 Supportも入れておくと.ttファイルがハイライトされて見やすいです
T4とは
T4はText Template Transformation Toolkitの略。
端的に言えば「Visual Studioでテキストファイルを自動生成するシステム」。
「いつファイルを生成するか」でおもに2通りある。
- 実行時 T4 テキスト テンプレート
- 「テンプレートに沿ったテキストを吐き出すクラス」を生成する。
- プログラム側から、そのクラスを明示的に利用することになる
- 前回利用したのはこっち
- 「テンプレートに沿ったテキストを吐き出すクラス」を生成する。
- デザイン時 T4 テキスト テンプレート
-
コンパイル前(≒テンプレート保存時)に、テンプレートに沿ったファイルを生成する。
- 基本的に、Visual Studio(VSCodeではない)が自動でやる
- ほぼC#(VB)のソースコード自動生成用
-
コンパイル前(≒テンプレート保存時)に、テンプレートに沿ったファイルを生成する。
Source Generatorを使う(失敗)
ソース ジェネレータープロジェクトの作成
> dotnet new classlib
生成されたcsprojの<TargetFramework>
をnetstandard2.0
に変更します。
また、<IsRoslynComponent>true</IsRoslynComponent>
を追加します。
<PropertyGroup>
- <TargetFramework>net6.0</TargetFramework>
+ <TargetFramework>netstandard2.0</TargetFramework>
+ <IsRoslynComponent>true</IsRoslynComponent>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
必要なパッケージを追加
dotnet add package
コマンドで必要なパッケージを追加します。
- Microsoft.CodeAnalysis.CSharp - ソースジェネレーター利用に必須
- Mono.TextTemplating.Roslyn - T4テンプレートのコンパイルに利用
追加したのち、csprojを以下のように編集します。
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsRoslynComponent>true</IsRoslynComponent>
+ <GetTargetPathDependsOn>$(GetTargetPathDependsOn);GetDependencyTargetPaths</GetTargetPathDependsOn>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.0.1" />
- <PackageReference Include="Mono.TextTemplating" Version="2.2.1" />
- <PackageReference Include="Mono.TextTemplating.Roslyn" Version="2.2.1" />
- <PackageReference Include="System.CodeDom" Version="4.4.0" />
+ <!-- 下記のTarget内で使うため、GeneratePathPropertyを設定する -->
+ <!-- 利用側では必要ないため、PrivateAssetsを設定する -->
+ <PackageReference Include="Mono.TextTemplating" Version="2.2.1" GeneratePathProperty="true" PrivateAssets="all" />
+ <PackageReference Include="Mono.TextTemplating.Roslyn" Version="2.2.1" GeneratePathProperty="true" PrivateAssets="all" />
+ <PackageReference Include="System.CodeDom" Version="4.4.0" GeneratePathProperty="true" PrivateAssets="all" />
</ItemGroup>
+
+ <Target Name="GetDependencyTargetPaths">
+ <ItemGroup>
+ <TargetPathWithTargetPlatformMoniker Include="$(PkgSystem_CodeDom)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
+ <TargetPathWithTargetPlatformMoniker Include="$(PkgMono_TextTemplating)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
+ <TargetPathWithTargetPlatformMoniker Include="$(PkgMono_TextTemplating_Roslyn)\lib\netstandard2.0\*.dll" IncludeRuntimeDependency="false" />
+ </ItemGroup>
+ </Target>
ISourceGeneratorの実装
using System;
using System.CodeDom.Compiler;
using System.IO;
using System.Linq;
using Microsoft.CodeAnalysis;
using Mono.TextTemplating;
using Location = Microsoft.CodeAnalysis.Location;
namespace T4Generator;
[Generator]
public class T4Generator : ISourceGenerator
{
// 今回のGeneratorには不要
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context)
{
// csprojの<AdditionalFiles>に一致したファイルはここに入る
var t4Files = context.AdditionalFiles.Where(at => at.Path.EndsWith(".tt"));
if (t4Files?.Any() != true) return;
// T4テンプレート変換クラスの生成+Roslynコンパイラーの使用
var generator = new TemplateGenerator();
generator.UseInProcessCompiler();
foreach (var file in t4Files)
{
var content = file.GetText(context.CancellationToken);
if (content is null) continue;
try
{
// T4テンプレートのコンパイル
var template = generator.CompileTemplate(content.ToString());
if (template is null)
{
// コンパイルエラー内容を取得し、ソースジェネレーター側にエラーを通知
var error = generator.Errors.Cast<CompilerError>().FirstOrDefault();
if (error is not null)
context.ReportDiagnostic(CreateT4ErrorDiagnostic(error));
continue;
}
// T4テンプレートの生成結果を取得
string sourceCode = template.Process();
// {ファイル名}.Generated.{ハッシュ}.csで保存
string fileName = Path.GetFileNameWithoutExtension(file.Path);
context.AddSource($"{fileName}.Generated.{file.GetHashCode():x8}.cs", sourceCode);
}
catch (Exception ex)
{
// ソースジェネレーター側にエラーを通知
if (ex is not OperationCanceledException)
context.ReportDiagnostic(Diagnostic.Create(_complieError, Location.None, ex));
}
}
static Diagnostic CreateT4ErrorDiagnostic(CompilerError err)
=> Diagnostic.Create(_invalidFileWarning, Location.None, err.ErrorNumber, err.ErrorText);
}
private static readonly DiagnosticDescriptor _complieError = new(
"T4GEN001",
"Couldn't parse T4 file",
"{0}",
"T4Generator",
DiagnosticSeverity.Warning,
true);
private static readonly DiagnosticDescriptor _invalidFileWarning = new(
"T4GEN002",
"Couldn't parse T4 file",
"{0}: {1}",
"T4Generator",
DiagnosticSeverity.Warning,
true);
}
ソース ジェネレーターを使うプロジェクトの作成
> dotnet new console
ソース ジェネレーターを参照する
+ <ItemGroup>
+ <!-- ソースジェネレーターでファイルを扱うには、AdditionalFilesが必要 -->
+ <AdditionalFiles Include="./**/*.tt" />
+ </ItemGroup>
+
+ <!-- プロジェクトをAnalyzerとして読み込む -->
+ <ItemGroup>
+ <ProjectReference Include="../../src/Nogic.T4Generator/Nogic.T4Generator.csproj" ReferenceOutputAssembly="false" OutputItemType="Analyzer" />
+ </ItemGroup>
T4ファイルを作成する
ついでにProgram.cs
も削除しています。
<#@ template hostspecific="false" language="C#" #>
<#@ output extension=".g.cs" #>
/// <auto-generated/>
<# string message = "Hello, World from T4!!!"; #>
Console.WriteLine("<#= message #>");
コンパイル(失敗)
> dotnet build
.NET 向け Microsoft (R) Build Engine バージョン 17.0.0+c9eb9dd64
Copyright (C) Microsoft Corporation.All rights reserved.
T4Generator -> T4Generator.dll
CSC : warning T4GEN001: System.IO.FileNotFoundException: Could not load file or assembly 'System.CodeDom, Version=4.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'. 指定されたファイルが見つかりません。 [Sample.csproj]
CSC : error CS5001: プログラムは、エントリ ポイントに適切な静的 'Main' メソッドを含んでいません [Sample.csproj]
ビルドに失敗しました。
CSC : warning T4GEN001: System.IO.FileNotFoundException: Could not load file or assembly 'System.CodeDom, Version=4.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'. 指定されたファイルが見つかりません。 [Sample.csproj]
CSC : error CS5001: プログラムは、エントリ ポイントに適切な静的 'Main' メソッドを含んでいません [Sample.csproj]
1 個の警告
1 エラー
System.CodeDom
のアセンブリが読み込めず失敗。
.NET ローカルツールを使う(成功)
プロジェクトの作成
省略
ローカルツールのインストール
> dotnet new tool-manifest
> dotnet tool install dotnet-t4
「ビルド前」「クリーンアップ後」の処理を追加
<ItemGroup>
<!-- T4ファイルをプロジェクトから参照する (TextTemplateは好きな名前でよい) -->
<TextTemplate Include="./**/*.tt" />
<!-- 生成後のファイルをプロジェクトから参照する (Generatedは好きな名前でよい) -->
<Generated Include="./**/*.g.cs" />
</ItemGroup>
<!-- ビルド前にdotnet-t4をTextTemplateに指定された各ファイルに対して呼び出すタスク -->
<Target Name="TextTemplateTransform" BeforeTargets="BeforeBuild">
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet t4 %(TextTemplate.FullPath)" />
</Target>
<!-- クリーンアップ後にGeneratedに指定されたファイルを削除するタスク -->
<Target Name="TextTemplateClean" AfterTargets="Clean">
<Delete Files="@(Generated)" />
</Target>
T4ファイルを作成する
ついでにProgram.cs
も削除しています。
<#@ template hostspecific="false" language="C#" #>
<# // extensionは、csprojのGeneratedで指定したものに合わせる #>
<#@ output extension=".g.cs" #>
/// <auto-generated/>
<# string message = "Hello, World from T4!!!"; #>
Console.WriteLine("<#= message #>");
ビルド
> dotnet build
ビルドプロセス上で、以下のファイルが生成される。
生成されたファイルも、Gitなどのバージョン管理に追加したほうがよさげ?(プロジェクトの方針による)
/// <auto-generated/>
Console.WriteLine("Hello, World from T4!!!");
既知の問題
初回ビルド時に失敗することがある
> dotnet build
.NET 向け Microsoft (R) Build Engine バージョン 17.0.0+c9eb9dd64
Copyright (C) Microsoft Corporation.All rights reserved.
復元対象のプロジェクトを決定しています...
復元対象のすべてのプロジェクトは最新です。
CSC : error CS5001: プログラムは、エントリ ポイントに適切な静的 'Main' メソッドを含んでいません [xxx.csproj]
ビルドに失敗しました。
CSC : error CS5001: プログラムは、エントリ ポイントに適切な静的 'Main' メソッドを含んでいません [xxx.csproj]
0 個の警告
1 エラー
-
Program.g.cs
がコンパイル対象になる前に、ビルド実行されてしまうのが原因? - もう一度ビルド実行することで解決する
T4ファイルが存在しない場合にエラー
> dotnet build
.NET 向け Microsoft (R) Build Engine バージョン 17.0.0+c9eb9dd64
Copyright (C) Microsoft Corporation.All rights reserved.
復元対象のプロジェクトを決定しています...
Input is empty
xxx.csproj(16,5): error MSB3073: コマンド "dotnet t4 " はコード 1 で終了しました。
-
<TextTemplate>
に当てはまるファイルが存在しない場合、dotnet-t4
を呼び出すコマンドに空の値が渡ってしまうことが原因 - T4を使わなくなった場合は、ちゃんと後片付けしましょう
Visual Studioとの共存
上記環境を構築すると、Visual Studio側からT4テンプレートをソリューションに追加する手法が取れなくなります。
また、すでにVisual Studio側にてT4テンプレートを使っている場合は、上記設定をすることでファイル生成が二重に行われてしまいます。
Visual Studio側の動作を止める場合は、<ItemGroup>
内にあるxxx.tt
を指定している項目を削除します。
まとめ
- ソースジェネレーターはいいぞ
- JSON Schemaから「Entityクラス」と「Web APIを呼び出すサービスクラス」とか作れそう
- ただ、NuGetパッケージまわりの項目はまだ難しい
- ソースジェネレーターで外部ファイルを読み込むには
<AdditionalFiles>
を追加する - ビルド周りで何かしたければMSBuildの黒魔術(というほどでもないけど)に頼る