LoginSignup
13
11

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-08-05
  • 追記(2019/8/8)
  • 追記2(2021/3/19)
    • .NET Core 3.1向けに改版しました。
    • DotNetCliToolReference非推奨となったため、.NET ローカル ツールを利用する方法に変更しました。

TL;DR

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

環境

T4とは

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

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

プロジェクトの前準備

dotnet-t4のインストール

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

.NET ローカル ツールについては個別記事も書いておりますので、そちらも参考にしてください。

プロジェクトの作成

> dotnet new console

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

TableInfoColumnInfoの定義
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; }
    }
}
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; }
    }
}

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

Mono.TextTemplatingを使います。

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

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

> dotnet add package Mono.TextTemplating

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

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

TextTemplateGenaratedを定義します。

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

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

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

%(TextTemplate.Identity)
foo/Template.ttfoo/Template.tt部分
$(RootNameSpace).%(TextTemplate.Filename)
デフォルトの名前空間.Template。名前空間がルートでない場合には個別に設定する
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を削除する。

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

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

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

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

TextTemplate.tt

<#@ 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() #>;
<# } #>
        }
    }
}

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

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

ITemplate.cs
using System;

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

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

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

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 ? "" : "?");
    }
}

プログラム側でテンプレートを呼び出す

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());
        }
    }
}

ビルド/実行

> dotnet build

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

> dotnet run
出力結果
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設定のジョブが動かない?

> 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

参考

13
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
11