.NET Core+VS CodeでもT4 テンプレートエンジンでコード生成したい！

- 追記(2019/8/8)
  - GitHubに[サンプルコード](https://github.com/nogic1008/T4Sample/tree/v2.0.0)を追加しました。
- 追記2(2021/3/19)
  - .NET Core 3.1向けに改版しました。
  - `DotNetCliToolReference`が[非推奨となった](https://github.com/dotnet/announcements/issues/107)ため、.NET ローカル ツールを利用する方法に変更しました。

## TL;DR

- .NET ローカル ツールとして[dotnet-t4](https://www.nuget.org/packages/dotnet-t4/)を追加
- `BeforeBuild`イベントで上記ツールを呼び出すよう設定する
- コンパイル エラーでビルドできない問題を「インターフェースのデフォルト実装」で潰す

## 環境
- Windows 10 Pro x64
  - Windows依存の操作をしていないので、Linuxとかでもできるはず？(未確認)
- .NET Core 3.1
  - 「[インターフェースのデフォルト実装](https://docs.microsoft.com/ja-jp/dotnet/csharp/tutorials/default-interface-members-versions)」と「[.NET ローカル ツール](https://docs.microsoft.com/ja-jp/dotnet/core/whats-new/dotnet-core-3-0#local-tools)」機能が必要なので、3.0以降(とC#8.0)が必須。
- Visual Studio Code
  - [C#拡張機能](https://marketplace.visualstudio.com/items?itemName=ms-vscode.csharp)を入れておきましょう
  - [T4 Support](https://marketplace.visualstudio.com/items?itemName=zbecknell.t4-support)も入れておくと.ttファイルがハイライトされて見やすいです

## T4とは
- [コード生成と T4 テキスト テンプレート](https://docs.microsoft.com/ja-jp/visualstudio/modeling/code-generation-and-t4-text-templates)

T4はText Template Transformation Toolkitの略。
端的に言えば「Visual Studioでテキストファイルを自動生成するシステム」。

例えば「テーブル定義ファイルからEntityクラスを自動生成する」のに便利。

## プロジェクトの前準備

### dotnet-t4のインストール

```console
> dotnet new tool-manifest
> dotnet tool install dotnet-t4
```

.NET ローカル ツールについては[個別記事](https://qiita.com/nogic1008/items/ce2dec260aa31bf205d8)も書いておりますので、そちらも参考にしてください。

### プロジェクトの作成

```console
> dotnet new console
```

### テーブル/列情報クラスの定義

<details><summary>`TableInfo`と`ColumnInfo`の定義</summary><div>

```csharp:TableInfo.cs
using System.Collections.Generic;

namespace T4Sample
{
    /// <summary>
    /// テーブル情報
    /// </summary>
    public class TableInfo
    {
        /// <summary>
        /// テーブル名
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 列
        /// </summary>
        public IEnumerable<ColumnInfo> Columns { get; set; }

        /// <summary>
        /// コメント
        /// </summary>
        public string Description { get; set; }
    }
}
```

```csharp:ColumnInfo.cs
using System.Collections.Generic;

namespace T4Sample
{
    /// <summary>
    /// 列情報
    /// </summary>
    public class ColumnInfo
    {
        /// <summary>
        /// 列名
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// 列のデータベース上での型
        /// </summary>
        public string Type { get; set; }

        /// <summary>
        /// 主キーか
        /// </summary>
        public bool IsPrimary { get; set; }

        /// <summary>
        /// nullを許可しないか
        /// </summary>
        public bool NotNull { get; set; }

        /// <summary>
        /// コメント
        /// </summary>
        public string Description { get; set; }
    }
}
```
</div></details>

## テンプレートエンジンの準備

[Mono.TextTemplating](https://github.com/mono/t4)を使います。

### Mono.TextTemplatingパッケージのインストール

テンプレート ファイルの生成するプログラムコードが、このパッケージに依存するため入れておく。

```console
> dotnet add package Mono.TextTemplating
```

### ビルド前、クリーンアップ後の処理を追加

#### ビルド前/クリーンアップ後の対象となるファイル群を定義

`TextTemplate`と`Genarated`を定義します。

```diff:T4Sample.csproj
  <ItemGroup>
     <PackageReference Include="Mono.TextTemplating" Version="2.2.1" />
+    <TextTemplate Include="**\*.tt" />
+    <Generated Include="**\*.Generated.cs" />
   </ItemGroup>
```

#### ビルド前に行う処理を定義

ビルド前に`dotnet t4`コマンドを呼び出すよう追記します。

<dl>
  <dt><code>%(TextTemplate.Identity)</code></dt>
  <dd><em>foo/Template.tt</em>の<em>foo/Template.tt</em>部分</dd>
  <dt><code>$(RootNameSpace)</code>.<code>%(TextTemplate.Filename)</code></dt>
  <dd>
    <em>デフォルトの名前空間.Template</em>。名前空間がルートでない場合には個別に設定する
  </dd>
</dl>

```diff:T4Sample.csproj
   </ItemGroup>
+  <Target Name="TextTemplateTransform" BeforeTargets="BeforeBuild">
+    <Exec WorkingDirectory="$(ProjectDir)" Command="dotnet t4 %(TextTemplate.Identity) -c $(RootNameSpace).%(TextTemplate.Filename) -o %(TextTemplate.Filename).Generated.cs" />
+  </Target>
```

#### クリーンアップ後の処理を定義

クリーンアップ(`dotnet clean`)後に、上記で生成した`*.Generated.cs`を削除する。

```diff:T4Sample.csproj
   </Target>
+  <Target Name="TextTemplateClean" AfterTargets="Clean">
+    <Delete Files="@(Generated)" />
+  </Target>
```

## テンプレート ファイルの作成

### テキスト テンプレート ファイル(*.tt)ファイルの作成

下記テンプレートで使用している`ToPascalCase()`と`ToCamelCase()`は、別の静的クラスで定義した`"snake_case"`を`"camelCase"`や`"PascalCase"`に変換する拡張メソッドです。
実装は割愛。

<details><summary>TextTemplate.tt</summary><div>

```csharp

<#@ template language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ output extension=".cs" #>
using System;
using System.Collections.Generic;

namespace <#= NameSpace #>
{
<# if (!string.IsNullOrEmpty(Table.Description)) { #>
    /// <summery>
    /// <#= Table.Description #>
    /// </summery>
<# } #>
    public class <#= Table.Name.ToPascalCase() #>
    {
<# foreach (var x in Table.Columns) { #>
<# if (!string.IsNullOrEmpty(x.Description)) { #>
        /// <summery>
        /// <#= x.Description #>
        /// </summery>
<# } // End if #>
        public <#= GetColumnType(x) #> <#= x.Name.ToPascalCase() #> { get; <#= x.IsPrimary ? "" : "set; " #>}

<# } // End foreach #>
        public <#= Table.Name.ToPascalCase() #>(
            <#= string.Join(",\n            ", Table.Columns.Where(x => x.IsPrimary).Select(d => $"{GetColumnType(d)} {d.Name.ToCamelCase()}")) #>
        )
        {
<# foreach (var x in Table.Columns.Where(x => x.IsPrimary)) { #>
            <#= x.Name.ToPascalCase() #> = <#= x.Name.ToCamelCase() #>;
<# } #>
        }

        public <#= Table.Name.ToPascalCase() #>(
            <#= string.Join(",\n            ", Table.Columns.Select(d => $"{GetColumnType(d)} {d.Name.ToCamelCase()}")) #>
        )
        {
<# foreach (var x in Table.Columns) { #>
            <#= x.Name.ToPascalCase() #> = <#= x.Name.ToCamelCase() #>;
<# } #>
        }
    }
}
```
</div></details>

### テンプレート生成前のデフォルト実装を用意する

`TransFormText()`の定義は、ビルド前に`t4`が生成する、TextTemplate.Generated.csに書かれる。
そのため、ビルド実行前には定義が存在しないので、コンパイル エラーとなってしまう。
このままではビルド前のコード生成も実行できないため、「インターフェースのデフォルト実装」を利用してごまかす。

```csharp:ITemplate.cs
using System;

namespace T4Sample
{
    public interface ITemplate
    {
        string TransformText() => throw new NotImplementedException();
    }
}
```

### テンプレートのpartialクラスを作成

テキスト テンプレートで利用するデータとメソッドを定義する。

```csharp:TextTemplate.cs
using System.Collections.Generic;

namespace T4Sample
{
    public partial class TextTemplate : ITemplate
    {
        /// <summary>
        /// DB上の型名⇒C#上の型を引き当てるためのDictionary。
        /// </summary>
        private readonly Dictionary<string, string> _typeDictionary;

        /// <summary>
        /// 名前空間
        /// </summary>
        public string NameSpace { get; }

        /// <summary>
        /// テーブル情報
        /// </summary>
        public TableInfo Table { get; }

        public TextTemplate(Dictionary<string, string> typeDictionary, string nameSpace, TableInfo table)
            => (_typeDictionary, NameSpace, Table) = (typeDictionary, nameSpace, table);

        /// <summary>
        /// 列情報⇒C#の型名
        /// </summary>
        public string GetColumnType(ColumnInfo column)
            => _typeDictionary.TryGetValue(column.Type, out var n) ? n : column.Type
                + (column.NotNull ? "" : "?");
    }
}
```
## プログラム側でテンプレートを呼び出す

```csharp:Program.cs
using System;
using System.Collections.Generic;

namespace T4Sample
{
    class Program
    {
        static void Main()
        {
            // DBの型⇒C#の型変換表 DBMSごとに1つ用意すればよい
            var typeDef = new Dictionary<string, string>()
            {
                { "integer", "int" },
                { "varchar", "string" },
                { "date", "DateTime" },
            };

            // テーブル情報 実際はDBなどから取得するものとする
            var table = new TableInfo()
            {
                Name = "shain_master",
                Description = "社員マスタ",
                Columns = new[]
                {
                    new ColumnInfo() { Name = "shain_id", Type = "integer", IsPrimary = true, NotNull = true },
                    new ColumnInfo() { Name = "shain_name", Type = "varchar", NotNull = true },
                    new ColumnInfo() { Name = "address", Type = "varchar", Description = "住所" },
                    new ColumnInfo() { Name = "created_date", Type = "date" },
                }
            };

            // 具象型で受けるとCS1061エラーが発生するため、インターフェースで受ける
            // インターフェースなので、override指定しなくてもTextTemplate側のTransformTextが呼ばれる
            ITemplate template = new TextTemplate(typeDef, "MyNameSpace", table);

            // 実際はファイル ストリームに書き出す
            Console.WriteLine(template.TransformText());
        }
    }
}
```

## ビルド/実行

```console
> dotnet build
```
`TextTemplate.Generated.cs`が生成され、それも含めてビルドされる。

```console
> dotnet run
```

```csharp:出力結果
using System;
using System.Collections.Generic;

namespace MyNameSpace
{
    /// <summery>
    /// 社員マスタ
    /// </summery>
    public class ShainMaster
    {
        public int ShainId { get; }

        public string ShainName { get; set; }

        /// <summery>
        /// 住所
        /// </summery>
        public string Address { get; set; }

        public DateTime CreatedDate { get; set; }

        public ShainMaster(
            int shainId
        )
        {
            ShainId = shainId;
        }

        public ShainMaster(
            int shainId,
            string shainName,
            string address,
            DateTime createdDate
        )
        {
            ShainId = shainId;
            ShainName = shainName;
            Address = address;
            CreatedDate = createdDate;
        }
    }
}
```

ちなみに横着して`dotnet build`せずに`dotnet run`すると以下のようになります。
`dotnet run`コマンド時のビルドプロセス時には`BeforeBuild`設定のジョブが動かない？

```console
> dotnet run
Unhandled exception. System.NotImplementedException: The method or operation is not implemented.
   at T4Sample.ITemplate.TransformText() in .\T4Sample\ITemplate.cs:line 7
   at T4Sample.Program.Main() in .\T4Sample\Program.cs:line 37
```

## 参考
- [T4 Templates at Build Time with Dotnet Core](https://notquitepure.info/2018/12/12/T4-Templates-at-Build-Time-With-Dotnet-Core/)