- 追記(2019/8/8)
- GitHubにサンプルコードを追加しました。
- 追記2(2021/3/19)
- .NET Core 3.1向けに改版しました。
-
DotNetCliToolReference
が非推奨となったため、.NET ローカル ツールを利用する方法に変更しました。
TL;DR
- .NET ローカル ツールとしてdotnet-t4を追加
-
BeforeBuild
イベントで上記ツールを呼び出すよう設定する - コンパイル エラーでビルドできない問題を「インターフェースのデフォルト実装」で潰す
環境
- Windows 10 Pro x64
- Windows依存の操作をしていないので、Linuxとかでもできるはず?(未確認)
- .NET Core 3.1
- 「インターフェースのデフォルト実装」と「.NET ローカル ツール」機能が必要なので、3.0以降(とC#8.0)が必須。
- Visual Studio Code
- C#拡張機能を入れておきましょう
- T4 Supportも入れておくと.ttファイルがハイライトされて見やすいです
T4とは
T4はText Template Transformation Toolkitの略。
端的に言えば「Visual Studioでテキストファイルを自動生成するシステム」。
例えば「テーブル定義ファイルからEntityクラスを自動生成する」のに便利。
プロジェクトの前準備
dotnet-t4のインストール
> dotnet new tool-manifest
> dotnet tool install dotnet-t4
.NET ローカル ツールについては個別記事も書いておりますので、そちらも参考にしてください。
プロジェクトの作成
> dotnet new console
テーブル/列情報クラスの定義
`TableInfo`と`ColumnInfo`の定義
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; }
}
}
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
ビルド前、クリーンアップ後の処理を追加
ビルド前/クリーンアップ後の対象となるファイル群を定義
TextTemplate
とGenarated
を定義します。
<ItemGroup>
<PackageReference Include="Mono.TextTemplating" Version="2.2.1" />
+ <TextTemplate Include="**\*.tt" />
+ <Generated Include="**\*.Generated.cs" />
</ItemGroup>
ビルド前に行う処理を定義
ビルド前にdotnet t4
コマンドを呼び出すよう追記します。
%(TextTemplate.Identity)
- foo/Template.ttのfoo/Template.tt部分
-
$(RootNameSpace)
.%(TextTemplate.Filename)
- デフォルトの名前空間.Template。名前空間がルートでない場合には個別に設定する
</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
を削除する。
</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に書かれる。
そのため、ビルド実行前には定義が存在しないので、コンパイル エラーとなってしまう。
このままではビルド前のコード生成も実行できないため、「インターフェースのデフォルト実装」を利用してごまかす。
using System;
namespace T4Sample
{
public interface ITemplate
{
string TransformText() => throw new NotImplementedException();
}
}
テンプレートのpartialクラスを作成
テキスト テンプレートで利用するデータとメソッドを定義する。
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 ? "" : "?");
}
}
プログラム側でテンプレートを呼び出す
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