【Visual Studio】【.NET】T4テキストテンプレートで文字列(ソースコード)を自動生成する方法まとめ

T4テキストテンプレートの基本的な使い方をまとめました。

T4テキストテンプレート?

T4 テキストテンプレートを使うと文字列を自動生成できます。
例えば以下のようなテンプレートを記述すると、

<#@ output extension=".txt" #>
<#
for(int i = 0; i < 100; i++)
{
    WriteLine($"{i}個目");
}
#>

以下のように100個の文字列が生成されます。

0個目
1個目
2個目
3個目
4個目
5個目
6個目
7個目
8個目
9個目
10個目
11個目
・
・
・
(略)
・
・
・
99個目

繰り返し記述する同じような処理を正確に、速く書くのに便利です。
実際にはこのようなテキストよりもソースコードの自動生成に使うことが多いです。

シンタックスハイライトとコード補間を有効にする

実際にテンプレートを使う前に、T4テキストテンプレートはデフォルトではシンタックスハイライトやコード補間が行われないため、拡張機能をインストールしておきます。
拡張機能はいくつかあるようですが今回はtangible T4 Editorを使用しました。

marketplace.visualstudio.com

テンプレートからコードを生成する

さてそれでは実際にT4テキストテンプレートを使っていきます。

デザイン時テキストテンプレートと実行時テキストテンプレート

T4テンプレートにはデザイン時テキストテンプレートと実行時テキストテンプレートが存在します。

デザイン時テキストテンプレートは最初の節で示したように、テンプレートで定義した通りに文字列を出力します。

これに対して実行時テキストテンプレートでは、まず中間ファイルとなるcsファイルが生成されます。
そしてこのcsファイルに定義されているクラスを以下のように使うことでテンプレートで定義した文字列が得られます。

// このRuntimeTextTemplate1が中間ファイルとして出力される
var runtimeTemplate = new RuntimeTextTemplate1();

// TransformText()でテンプレートで定義した文字列を取得できる。
var text = runtimeTemplate.TransformText();

このようにランタイムテンプレートを使って中間ファイルを作っておけば、
T4 テンプレートの実行環境がない環境でもテンプレートから処理を生成することができます。

デザイン時テンプレートを生成する

さてそれではデザイン時テキストテンプレートを作成します。
ソリューションエクスプローラからプロジェクトを右クリック > 追加 > 新しい項目を追加を選択します。

f:id:halya_11:20200723104229p:plain
新しい項目を追加

ウィンドウが開くのでテキストテンプレートを選択して追加します。

f:id:halya_11:20200723104400p:plain
テキストテンプレートを追加

拡張子がttのファイルが作成され、以下のように記述されていることが確認できます。

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".txt" #>

試しにこのテンプレートを最初の節で示した内容に書き換えてみます。

<#@ output extension=".txt" #>
<#
for(int i = 0; i < 100; i++)
{
    WriteLine($"{i}個目");
}
#>

この状態で保存すると、ttファイルの子として生成されているtxtファイルが書き換わります。

f:id:halya_11:20200723104630p:plain
ttファイルとtxtファイル

txtファイルに100個の文字列が記述されていたら成功です。

実行時テキストテンプレートを生成する

次に実行時テキストテンプレートを作成します。
デザイン時テンプレートと同様にソリューションエクスプローラからプロジェクトを右クリック > 追加 > 新しい項目を追加を選択します。
ウィンドウが開くのでランタイム テキストテンプレートを選択して追加します。

f:id:halya_11:20200723104927p:plain
ランタイムテキストテンプレートを追加

拡張子がttのファイルが作成されるので、以下のように書き換えます。

<#@ output extension=".txt" #>
<#
for(int i = 0; i < 100; i++)
{
    WriteLine($"{i}個目");
}
#>

するとcsファイルに以下のような長いコードが自動生成されます(かなり省略してます)。

// ------------------------------------------------------------------------------
// <auto-generated>
//     このコードはツールによって生成されました。
//     ランタイム バージョン: 15.0.0.0
//  
//     このファイルへの変更は、正しくない動作の原因になる可能性があり、
//     コードが再生成されると失われます。
// </auto-generated>
// ------------------------------------------------------------------------------
namespace TemplateExample
{
    using System;
    
    /// <summary>
    /// Class to produce the template output
    /// </summary>
    
    #line 1 "F:\Documents\TemplateExample\TemplateExample\RuntimeTextTemplate1.tt"
    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.TextTemplating", "15.0.0.0")]
    public partial class RuntimeTextTemplate1 : RuntimeTextTemplate1Base
    {
        ..(略)...
    }
    
    #line default
    #line hidden
    #region Base class
        ..(略)...
    #endregion
}

上記のコードにはRuntimeTextTemplate1というテンプレートと同名のクラスが定義されていることが確認できます。
これを以下のようにして使います。

using System;

namespace TemplateExample
{
    class Program
    {
        static void Main(string[] args)
        {
            // ランタイムテンプレートからテキストを生成
            var text = new RuntimeTextTemplate1().TransformText();

            Console.WriteLine(text);
            Console.ReadLine();

        }
    }
}

すると、コンソールにテキストテンプレートのときと同様の100個の文字列が出力されることを確認できます。

基本的な使い方

次にテンプレートの基本的な使い方を学びます。

言語・拡張子・文字コードを指定する

言語の指定は以下のように行います。
このように<#@と#>で囲まれた部分をディレクティブと呼びます。

<#@ template language="C#" #>

拡張子は以下のようにoutputディレクティブで指定します。
デフォルトはcsなのでcsファイルを出力するときは書かなくてもOKです。

<#@ output extension=".cs" #>

文字コードもoutputディレクティブで指定します。
デフォルトはそのファイルの文字コードになります。

<#@ output encoding="utf-8"#>
文字列を書く

出力ファイルにそのまま書きだしたい文字列は普通にそのまま記述します。

<#@ template language="C#" #>
public class Example
{
    public int test;
}

このテンプレートは以下のような出力結果となります。

public class Example
{
    public int test;
}
制御構文や関数などを使う - 標準コントロールブロック

<#と#>で囲まれた標準コントロールブロックでは制御構文や関数などを使用できます。

<#@ template language="C#" #>
public class Example
{
<#
    // インデントを追加するユーティリティメソッド
    PushIndent("    ");
    for(int i = 0; i < 10; i++)
    {
        // 文字列を出力して改行するユーティリティメソッド
        WriteLine($"public int testInt{i};");
    }
    // インデントを解除
    ClearIndent();
#>
}

このテンプレートの出力は以下の通りとなります。

public class Example
{
    public int testInt0;
    public int testInt1;
    public int testInt2;
    public int testInt3;
    public int testInt4;
    public int testInt5;
    public int testInt6;
    public int testInt7;
    public int testInt8;
    public int testInt9;
}
式コントロールブロックを使った書き方

<#=と#>で囲まれた式コントロールブロックに変数を記述するとその部分が結果に出力されます。
前節のコードを機コントロールブロックを使って書き換えると以下のようになります。
(今回のケースではWriteLine()を使ったほうがすっきりした書き方になりますが)

<#@ template language="C#" #>
public class Example
{
<#
    // インデントを追加するユーティリティメソッド
    PushIndent("    ");
    for(int i = 0; i < 10; i++)
    {
#>
public int testInt<#= i #>;
<#
    }
    // インデントを解除
    ClearIndent();
#>
}
テンプレート内で使う機能を定義する - クラス機能コントロールブロック

テンプレート内で使う関数や変数を定義するためには、<#+と#>で囲まれたクラス機能コントロールブロックを使います。

<#@ template language="C#" #>
public class Example
{
<#
    // インデントを追加するユーティリティメソッド
    PushIndent("    ");
    for(int i = 0; i < 10; i++)
    {
        WriteLine(GetFieldText(i));
    }
    // インデントを解除
    ClearIndent();
#>
}

<#+
private const string FieldText = "public int testInt{0};";
private string GetFieldText(int id)
{
    return string.Format(FieldText, id);
}
#>

クラス機能コントロールブロックはかならずファイルの下部(それを使う部分よりも下)に定義する必要があるので注意してください。

知っておくべき使い方

次にその他知っておくべき使い方をまとめておきます。

アセンブリを読み込む - assemblyディレクティブ

T4テンプレートではSystem.dllなど一部のアセンブリは自動的に読み込まれますが、
それ以外のアセンブリを参照したい場合にはassemblyディレクティブを使う必要があります。

ためしに手元にあったGoogle.Apis.dllを読み込んでみます。
以下の例では絶対パスで読み込んでいますが($SolutionDir)などVisual Studioの変数も使えます。
またGAC(C:\Windows\assemblyあたりにあるdll)についてはSystem.Xml.dllのように名前だけで読み込めます。

<#@ template language="C#" #>
<#@ assembly name="C:\Users\username\.nuget\packages\google.apis\1.45.0\lib\net45\Google.Apis.dll" #>

public enum <#= nameof(Google.Apis.Upload.UploadStatus) #>
{
<#
    PushIndent("    ");
    foreach (var item in Enum.GetValues(typeof(Google.Apis.Upload.UploadStatus)))
    {
        WriteLine($"{item.ToString()},");
    }
    ClearIndent();
#>
}

UploadStatusという列挙型の複製を生成しているだけです。 結果は以下のようになります。

public enum UploadStatus
{
    NotStarted,
    Starting,
    Uploading,
    Completed,
    Failed,
}

ちなみに実行時テンプレートの場合には、assemblyディレクティブは必要ない(補間のためにあってもいい)ですが、
Visual Studioプロジェクトの参照セクションに当該アセンブリを加えておく必要があります。

f:id:halya_11:20200722230423p:plain
参照に当該アセンブリ

名前空間のusing的なことをする - importディレクティブ

前節の例ではGoogle.Apis.Upload.UploadStatusのようにいちいち書いていましたが、importディレクティブを使うとusing的なことができます。

<#@ template language="C#" #>
<#@ assembly name="C:\Users\username\.nuget\packages\google.apis\1.45.0\lib\net45\Google.Apis.dll" #>
<#@ import namespace="Google.Apis.Upload" #>

public enum <#= nameof(UploadStatus) #>
{
<#
    PushIndent("    ");
    foreach (var item in Enum.GetValues(typeof(UploadStatus)))
    {
        WriteLine($"{item.ToString()},");
    }
    ClearIndent();
#>
}

Google.Apis.Upload.UploadStatusと書いていたところがUploadStatusと書けるようになりました。

実行時テンプレートからテキストを作る際に入力値を与える

テンプレートからテキストを作る際に入力値を与えたいことがあります。
例えば以下のようなクラスを定義するテンプレートにおいて、ExampleClassの部分を外からの入力値によって置換したいとします。

<#@ template language="C#" #>
public class ExampleClass
{
}

このようなケースでは、この実行時テンプレートから生成されたcsファイルに定義されたクラスのpartial classを別ファイルに定義し、
そこにテンプレートに受け渡したい変数と、それを入力するためのコンストラクタを定義します。

namespace TemplateExample
{
    // Partialクラス
    public partial class RuntimeTextTemplate1
    {
        // 外から受け取るクラス名
        private string _className;

        // コンストラクタ
        public RuntimeTextTemplate1(string className) { 
            _className = className;
        }
    }
}

さらにテンプレートでは以下のようにしてこの変数を使用します。

<#@ template language="C#" #>
public class <# Write(_className); #>
{
}

このテンプレートを以下のように使用します。

using System;

namespace TemplateExample
{
    class Program
    {
        static void Main(string[] args)
        {
            // クラス名を与えつつランタイムテンプレートからテキストを生成
            var text = new RuntimeTextTemplate1("ExampleClass").TransformText();

            Console.WriteLine(text);
            Console.ReadLine();
        }
    }
}

すると以下のような出力が得られることが確認できます。

public class ExampleClass{
}

正常に入力値が受け渡せていることを確認できました。

なお、parameterディレクティブを使う方法もありますが、
これはどちらかというとデザイン時テキストテンプレートのための機能という意味合いが強いこともあり今回は省略します。

docs.microsoft.com

ファイルからテンプレート用テキストをインクルード - includeディレクティブ

includeディレクティブを使うと共通のメソッドなどを別ファイルに切り出せます。
例えばいま、ExampleInclude.t4というファイルを作成し、以下のようにメソッドを記述します。
include用の拡張子は何でも問題ない(ただしttはあまりよくない)ですが今回はt4にしています。

<#+
private const string FieldText = "public int includeTest{0};";

private string GetFieldText(int id)
{
    return string.Format(FieldText, id);
}
#>

次にこれをttファイルからインクルードし、GetFieldText()メソッドを使用してみます。

<#@ template language="C#" #>
<# /* ExampleInclude.t4をインクルード */ #>
<#@ include file="ExampleInclude.t4" once="true" #>
public class Example
{
<#
    PushIndent("    ");
    for(int i = 0; i < 10; i++)
    {
        // ExampleInclude.t4に定義されているメソッドを使う
        WriteLine(GetFieldText(i));
    }
    ClearIndent();
#>
}

fileにはインクルードするファイルの相対パス絶対パスを指定します。
onceは二重インクルードを防止するためのオプションです。

エラーや警告を出す

テンプレート変換時のエラーを出力するにはError()メソッドを使います。

<#@ template language="C#" #>
public class Example
{
<#
    PushIndent("    ");
    for(int i = 0; i < 10; i++)
    {
        var fieldText = GetFieldText(i);
        if (string.IsNullOrEmpty(fieldText))
        {
            // テンプレートの変換時エラーにする
            Error($"{nameof(fieldText)} is null or empty.");
        }
        WriteLine(GetFieldText(i));
    }
    ClearIndent();
#>
}

<#+
private const string FieldText = "public int includeTest{0};";

private string GetFieldText(int id)
{
    return string.Format(FieldText, id);
}
#>

警告にはWarning()を使います。

参考

docs.microsoft.com