Help us understand the problem. What is going on with this article?

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

追記(2019/8/8) GitHubにサンプルコードを追加しました。

TL;DR

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

環境

  • Windows 10 Pro x64
    • Windows依存の操作をしていないので、Linuxとかでもできるはず?(未確認)
  • .NET Core 3.0 Preview 7
  • Visual Studio Code
    • C#拡張機能を入れておきましょう
    • T4 Supportも入れておくと.ttファイルがハイライトされて見やすいです

T4とは

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

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

プロジェクトの前準備

プロジェクトの作成

> dotnet new console

.csprojファイルの編集

  • 「インターフェースのデフォルト実装」がこの記事の方法では必須なので、LangVersionに8.0以降を指定します
T4Sample.csproj
   <PropertyGroup>
+    <LangVersion>8.0</LangVersion>
     <OutputType>Exe</OutputType>
     <TargetFramework>netcoreapp3.0</TargetFramework>
   </PropertyGroup>

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

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 --version 2.0.5

コマンドラインツールのインストール

T4Sample.csproj
  <ItemGroup>
+    <DotNetCliToolReference Include="dotnet-t4-project-tool" Version="2.0.5" />
     <PackageReference Include="Mono.TextTemplating" Version="2.0.5" />
   </ItemGroup>

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

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

TextTemplateGenaratedを定義します。

T4Sample.csproj
  <ItemGroup>
     <DotNetCliToolReference Include="dotnet-t4-project-tool" Version="2.0.5" />
     <PackageReference Include="Mono.TextTemplating" Version="2.0.5" />
+    <TextTemplate Include="**\*.tt" />
+    <Generated Include="**\*.Generated.cs" />
   </ItemGroup>

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

ビルド前にdotnet t4コマンド(DotNetCliReferenceに定義したツール)を呼び出すよう追記します。

%(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>

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

クリーンアップ後に、上記で生成した*.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" #>
<#@ import namespace="System.Collections.Generic" #>
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>
<#
            }
#>
        public <#= GetColumnType(x) #> <#= x.Name.ToPascalCase() #> { get; <#= x.IsPrimary ? "" : "set; " #>}

<#
        }
#>
        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

参考

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away