Visual Studio Advent Calendar 2014 第 17 日目は知っている方にはおなじみの内容ですが、あらためて T4 テンプレートエンジンによるコード自動生成の1案をご紹介します
T4:Text Template Transformation Toolkit とは
- Visual Studio と連携できるファイル自動生成テキストテンプレートエンジン
- 拡張子 *.tt のテンプレートファイルを読み込んでテキストファイルを自動生成できます
- テキストブロックとロジックブロックを混在させる ASP のような記述方式
テキストファイルのテンプレートエンジンには Java では Velocity や JET などがありますが、.NET では T4 というテンプレートエンジンがあります
Velocity や JET などと比べると IDE である Visual Studio との連携が強力で、細かい設定をしなくてもテンプレートの追加や生成ロジックの記述、コード生成の実行などが簡単に行えます
テンプレートファイルは生成コードが必要なプロジェクトで「新しい項目の追加」ダイアログを開き、「テキストテンプレート」(拡張子 *.tt)を選べば追加できます
<#@ template language="C#" #>
<html><body>
<h1>Sales for Previous Month</h2>
<table>
<# for (int i = 1; i <= 10; i++)
{ #>
<tr><td>Test name <#= i #> </td>
<td>Test value <#= i * i #> </td> </tr>
<# } #>
</table>
This report is Company Confidential.
</body></html>
中身はこんな感じで ASP のコードを書くように記述できます
詳しい文法などは各所でいろいろと紹介されているのでご参考ください
MSDN コード生成と T4 テキスト テンプレート
http://msdn.microsoft.com/ja-jp/library/bb126445.aspx
Visual Studio搭載のT4テンプレートエンジンの3通りの活用方法
http://d.hatena.ne.jp/seraphy/20140419
T4 Template 入門
http://www.slideshare.net/Posaune/t4-template
[PPT] T4 で簡単に疎結合
http://www.mnow.jp/LinkClick.aspx?fileticket=xnbxsBYiULU%3D&tabid=220&mid=867
*.tt ファイルに生成ロジックを記述して保存すると自動でコードが生成されますが、ファイルを選んで「カスタム ツールの実行」を選ぶことで明示的に再生成できます
ビルドメニューの「すべての T4 テンプレートの変換」を選ぶことでソリューション内のすべての *.tt ファイルの生成を一括して行うこともできます
こういった機能がデフォルトで利用できる点が T4 テンプレートおよび Visual Studio の便利な点だと思います
アセンブリを参照して T4 でコード生成
- 通常の T4 記述では拡張機能を導入しないとコード補完が利きません
- 構成定義と *.tt ファイルの分離により再利用性の高いコード生成ができます
- 構成定義に属性記述を利用すれば簡潔に記載できます
さて、ここからが本題
Visual Studio を使えば手軽にコード生成ができるのだから、もっと本格的に T4 テンプレートを活用する方法を考えてみました
概要としては上記の通りで、通常 *.tt ファイル内に記載する内容の一部を外部のアセンブリ(DLL ファイル)から参照する形にしただけです
外部アセンブリに埋め込む情報は、入力効率を高めるためにクラスに付与する属性として記述する形にしています
こうすることで、*.tt ファイル内の冗長な記述(繰り返しのある部分)などをさらに共通化することができるというわけです
例えば、冗長で多くのクラスが必要になりやすい Model を生成するために次のようなプロジェクト構成をもつソリューションを作ってみます
T4Sample.GenerateDefine はクラス属性定義を含む DLL がビルドできるプロジェクトで、T4Sample.Windows はこの DLL を参照して T4 テンプレートで Model クラスを自動生成して利用するプロジェクト(Windows ストアアプリ)です
T4Sample.GenerateDefine プロジェクトのビルド設定にある「出力パス」には T4Sample.Windows プロジェクトの InfrastructureAssemblies フォルダを相対パスで指定することで、ビルド後にできる T4Sample.GenerateDefine.dll を T4Sample.Windows プロジェクトの T4 テンプレートから参照できるようにしておきます
/// <summary>
/// クラス定義情報
/// </summary>
public class ClassDefinition
{
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="name">クラス名称</param>
/// <param name="baseType">基底クラスの型</param>
/// <param name="description">クラスの説明</param>
/// <param name="propertyAttributes">プロパティ属性</param>
/// <param name="attributes">付帯属性</param>
public ClassDefinition(string name, Type baseType, string description, PropertyAttribute[] propertyAttributes,
params string[] attributes)
{
this.Name = name;
this.BaseBaseTypeName = baseType.Name;
this.Description = description;
this.PropertyAttributes = propertyAttributes;
this.Attributes = attributes;
}
/// <summary>
/// クラス名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 基底クラスの型
/// </summary>
public string BaseBaseTypeName { get; set; }
/// <summary>
/// クラスの説明
/// </summary>
public string Description { get; set; }
/// <summary>
/// プロパティ属性
/// </summary>
public PropertyAttribute[] PropertyAttributes { get; set; }
/// <summary>
/// 付帯属性
/// </summary>
public string[] Attributes { get; set; }
}
/// <summary>
/// 説明文属性
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class DescriptionAttribute : Attribute
{
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="description">説明文</param>
public DescriptionAttribute(string description)
{
this.Description = description;
}
/// <summary>
/// プロパティ名称
/// </summary>
public string Description { get; set; }
}
/// <summary>
/// 付帯属性属性
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
public class ClassAttributeAttribute : Attribute
{
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="attribute">説明文</param>
public ClassAttributeAttribute(string attribute)
{
this.Attribute = attribute;
}
/// <summary>
/// プロパティ名称
/// </summary>
public string Attribute { get; set; }
}
これはクラスに付与する属性の定義クラスです
C# なら属性として付与する情報の内容も上記のようにカスタマイズできますね
/// <summary>
/// プロパティ自動生成設定属性
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
public class PropertyAttribute : Attribute
{
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="name">プロパティ名称</param>
/// <param name="type">プロパティの型</param>
/// <param name="displayName">プロパティの説明</param>
/// <param name="attributes">付帯属性</param>
public PropertyAttribute(string name, Type type, string displayName, params string[] attributes)
{
var textInfo = System.Globalization.CultureInfo.CurrentCulture.TextInfo;
this.Name = textInfo.ToUpper(name.ToCharArray().FirstOrDefault<char>()) + name.Substring(1);
this.FieldName = textInfo.ToLower(name.ToCharArray().FirstOrDefault<char>()) + name.Substring(1);
this.TypeName = GetTypeName(type);
this.IsCollection = !type.IsArray && type.GetTypeInfo().ImplementedInterfaces.Contains(typeof(ICollection));
this.DisplayName = displayName;
this.Attributes = attributes;
}
/// <summary>
/// プリミティブ型の表記置換表
/// </summary>
private static readonly Dictionary<string, string> Primitives = new Dictionary<string, string>()
{
{typeof(object).Name, "object"},
{typeof(bool).Name, "bool"},
{typeof(byte).Name, "byte"},
{typeof(sbyte).Name, "sbyte"},
{typeof(char).Name, "char"},
{typeof(string).Name, "string"},
{typeof(int).Name, "int"},
{typeof(uint).Name, "uint"},
{typeof(short).Name, "short"},
{typeof(ushort).Name, "ushort"},
{typeof(long).Name, "long"},
{typeof(ulong).Name, "ulong"},
{typeof(double).Name, "double"},
{typeof(float).Name, "float"},
{typeof(decimal).Name, "decimal"},
};
/// <summary>
/// 型の名前を文字列化する
/// </summary>
/// <param name="type">変換対象の型</param>
/// <returns>型の名前</returns>
private static string GetTypeName(Type type)
{
var name = type.Name;
var typeInfo = type.GetTypeInfo();
if (type == typeof(string) || typeInfo.IsPrimitive)
{
return Primitives[name];
}
if (typeInfo.IsArray)
{
return string.Format("{0}{1}", GetTypeName(type.GetElementType()), string.Concat(Enumerable.Repeat("[]", type.GetArrayRank()).ToArray()));
}
if (!typeInfo.IsGenericType)
{
return name;
}
if (type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
return string.Format("{0}?", type.GenericTypeArguments.Select(GetTypeName).FirstOrDefault());
}
return string.Format("{0}<{1}>", name.Substring(0, name.IndexOf('`')), string.Join(",", type.GenericTypeArguments.Select(GetTypeName)));
}
/// <summary>
/// プロパティ名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// プロパティメンバ変数名称
/// </summary>
public string FieldName { get; set; }
/// <summary>
/// プロパティの型
/// </summary>
public string TypeName { get; set; }
/// <summary>
/// コレクション型フラグ
/// </summary>
public bool IsCollection { get; set; }
/// <summary>
/// プロパティの日本語名
/// </summary>
public string DisplayName { get; set; }
/// <summary>
/// 付帯属性
/// </summary>
public string[] Attributes { get; set; }
}
こちらはクラス属性の中に含まれるクラスのプロパティ情報を属性として付与するための定義クラスです
プロパティの型はコードに記載するときに Camel 形式で生成できるよう、型と表記の置換表を持たせています
上記のようなクラス構成情報の定義クラスを利用して Model を自動生成する定義クラスを次のように記述します
/// <summary>
/// Model 自動生成定義クラス
/// </summary>
public static class ModelDefinition
{
/// <summary>
/// Model の自動生成定義を取得する
/// </summary>
/// <returns>Model の自動生成定義</returns>
public static IEnumerable<ClassDefinition> GetDefinitions()
{
var types = typeof(ModelDefinition).GetTypeInfo().Assembly.DefinedTypes.Where(
ti => ti.IsClass
& !ti.IsAbstract
&& !ti.IsValueType
&& ti.GetCustomAttributes<PropertyAttribute>().Any()
&& !ti.IsSubclassOf(typeof(ViewModelBase)));
return types.Select(
t => new ClassDefinition(
t.Name,
t.BaseType,
t.GetCustomAttributes<DescriptionAttribute>().Any() ? t.GetCustomAttributes<DescriptionAttribute>().First().Description : string.Empty,
t.GetCustomAttributes<PropertyAttribute>().ToArray(),
t.GetCustomAttributes<ClassAttributeAttribute>().Any() ? t.GetCustomAttributes<ClassAttributeAttribute>().Select<ClassAttributeAttribute, string>(a => a.Attribute).ToArray() : null));
}
/// <summary>
/// 永続化リポジトリの自動生成定義を取得する
/// </summary>
/// <returns>Model の自動生成定義</returns>
public static IEnumerable<RepositoryDefinition> GetRepositoryDefinitions()
{
var types = typeof(ModelDefinition).GetTypeInfo().Assembly.DefinedTypes.Where(
ti => ti.IsClass
& !ti.IsAbstract
&& !ti.IsValueType
&& ti.GetCustomAttributes<RepositoryAttribute>().Any()
&& !ti.IsSubclassOf(typeof(ViewModelBase)));
return types.Select(
t => new RepositoryDefinition(
t.Name,
t.GetCustomAttributes<DescriptionAttribute>().Any() ? t.GetCustomAttributes<DescriptionAttribute>().First().Description : t.Name,
t.GetCustomAttributes<RepositoryAttribute>().FirstOrDefault()));
}
}
[Description("写真情報の Model")]
[Property("uniqueId", typeof(string), "ID", "[XmlAttribute(\"id\")]")]
[Property("imageUri", typeof(string), "画像Uri", "[XmlAttribute(\"image\")]")]
[Property("title", typeof(string), "タイトル", "[XmlAttribute(\"title\")]")]
[Property("owner", typeof(string), "撮影者", "[XmlAttribute(\"owner\")]")]
class Photo
{
}
情報として重要なのは最後の Photo クラスとそのクラス属性です
属性付与により Photo Model の説明や持っているプロパティを簡潔に記述できます
このコードをビルドすることで、Photo Model の構成情報が DLL ファイルに埋め込まれます
お次は DLL を参照してコードを自動生成する側の T4Sample.Windows プロジェクトについて
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Runtime" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Reflection" #>
<#@ assembly name="$(ProjectDir)..\T4Sample.Windows\InfrastructureAssemblies\T4Sample.GenerateDefine.dll" #>
<#@ import namespace="T4Sample.GenerateDefine" #>
<#@ output extension=".generated.cs" #>
//<auto-generated>
#region License
//-----------------------------------------------------------------------
// <copyright>
// Copyright matatabi-ux 2014.
// </copyright>
//-----------------------------------------------------------------------
#endregion
namespace T4Sample.Models
{
<# PushIndent(" "); #>
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Serialization;
using Microsoft.Practices.Prism.Mvvm;
<#
foreach (var m in ModelDefinition.GetDefinitions())
{
#>
<# if (!string.IsNullOrEmpty(m.Description)) { #>
/// <summary>
/// <#= m.Description #>
/// </summary>
<# }
if (m.Attributes != null)
{
foreach(var attribute in m.Attributes)
{#>
<#= attribute #>
<# }
} #>
public partial class <#= m.Name #>
{<#
PushIndent(" "); #>
#region マルチスレッド排他制御用
/// <summary>
/// 排他制御フラグ
/// </summary>
private static bool isSynchronize = false;
/// <summary>
/// 同期コンテキスト
/// </summary>
private static readonly SynchronizationContext SyncContext = SynchronizationContext.Current;
/// <summary>
/// 排他制御フラグ
/// </summary>
[XmlIgnore]
public static bool IsSynchronize
{
get { return isSynchronize; }
set { isSynchronize = value; }
}
/// <summary>
/// 排他制御オブジェクト
/// </summary>
public static readonly object LockObject = new object();
/// <summary>
/// ロック
/// </summary>
public void Lock()
{
SyncContext.Post((e) =>
{
Monitor.Enter(LockObject);
}, null);
}
/// <summary>
/// ロック解除
/// </summary>
public void Unlock()
{
SyncContext.Post((e) =>
{
Monitor.Exit(LockObject);
}, null);
}
#endregion //マルチスレッド排他制御用
<# if (m.PropertyAttributes != null)
{
foreach(var property in m.PropertyAttributes)
{ #>
#region <#= property.Name #>:<#= property.DisplayName #> プロパティ
<# if (!string.IsNullOrEmpty(property.DisplayName))
{ #>
/// <summary>
/// <#= property.DisplayName #>
/// </summary>
<# } #>
private <#= property.TypeName #> <#= property.FieldName #><# if ( property.IsCollection ) { #> = new <#= property.TypeName #>()<# } #>;
<# if (!string.IsNullOrEmpty(property.DisplayName))
{ #>
/// <summary>
/// <#= property.DisplayName #> の取得および設定
/// </summary>
<# }
if (property.Attributes != null)
{
foreach(var attribute in property.Attributes)
{#>
<#= attribute #>
<# }
} #>
public <#= property.TypeName #> <#=property.Name #>
{
get { return this.<#= property.FieldName #>; }
set
{
try
{
if (isSynchronize)
{
this.Lock();
}
<#= property.FieldName #> = value;
}
finally
{
if (isSynchronize)
{
this.Unlock();
}
}
}
}
#endregion //<#= property.Name #>:<#= property.DisplayName #> プロパティ
<# }
}
PopIndent(); #>
}
<#
}
PopIndent(); #>
}
Model を自動生成する T4 テンプレートはこんな感じ
<#@ assembly name="$(ProjectDir)..\T4Sample.Windows\InfrastructureAssemblies\T4Sample.GenerateDefine.dll" #>
DLL ファイルの参照は上記の部分で設定しています
「カスタムツール の実行」で T4 テンプレートの変換を行うと・・・
//<auto-generated>
#region License
//-----------------------------------------------------------------------
// <copyright>
// Copyright matatabi-ux 2014.
// </copyright>
//-----------------------------------------------------------------------
#endregion
namespace T4Sample.Models
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Serialization;
using Microsoft.Practices.Prism.Mvvm;
/// <summary>
/// 写真情報の Model
/// </summary>
public partial class Photo
{
#region マルチスレッド排他制御用
/// <summary>
/// 排他制御フラグ
/// </summary>
private static bool isSynchronize = false;
/// <summary>
/// 同期コンテキスト
/// </summary>
private static readonly SynchronizationContext SyncContext = SynchronizationContext.Current;
/// <summary>
/// 排他制御フラグ
/// </summary>
[XmlIgnore]
public static bool IsSynchronize
{
get { return isSynchronize; }
set { isSynchronize = value; }
}
/// <summary>
/// 排他制御オブジェクト
/// </summary>
public static readonly object LockObject = new object();
/// <summary>
/// ロック
/// </summary>
public void Lock()
{
SyncContext.Post((e) =>
{
Monitor.Enter(LockObject);
}, null);
}
/// <summary>
/// ロック解除
/// </summary>
public void Unlock()
{
SyncContext.Post((e) =>
{
Monitor.Exit(LockObject);
}, null);
}
#endregion //マルチスレッド排他制御用
#region ImageUri:画像Uri プロパティ
/// <summary>
/// 画像Uri
/// </summary>
private string imageUri;
/// <summary>
/// 画像Uri の取得および設定
/// </summary>
[XmlAttribute("image")]
public string ImageUri
{
get { return this.imageUri; }
set
{
try
{
if (isSynchronize)
{
this.Lock();
}
imageUri = value;
}
finally
{
if (isSynchronize)
{
this.Unlock();
}
}
}
}
#endregion //ImageUri:画像Uri プロパティ
#region UniqueId:ID プロパティ
/// <summary>
/// ID
/// </summary>
private string uniqueId;
/// <summary>
/// ID の取得および設定
/// </summary>
[XmlAttribute("id")]
public string UniqueId
{
get { return this.uniqueId; }
set
{
try
{
if (isSynchronize)
{
this.Lock();
}
uniqueId = value;
}
finally
{
if (isSynchronize)
{
this.Unlock();
}
}
}
}
#endregion //UniqueId:ID プロパティ
#region Title:タイトル プロパティ
/// <summary>
/// タイトル
/// </summary>
private string title;
/// <summary>
/// タイトル の取得および設定
/// </summary>
[XmlAttribute("title")]
public string Title
{
get { return this.title; }
set
{
try
{
if (isSynchronize)
{
this.Lock();
}
title = value;
}
finally
{
if (isSynchronize)
{
this.Unlock();
}
}
}
}
#endregion //Title:タイトル プロパティ
#region Owner:撮影者 プロパティ
/// <summary>
/// 撮影者
/// </summary>
private string owner;
/// <summary>
/// 撮影者 の取得および設定
/// </summary>
[XmlAttribute("owner")]
public string Owner
{
get { return this.owner; }
set
{
try
{
if (isSynchronize)
{
this.Lock();
}
owner = value;
}
finally
{
if (isSynchronize)
{
this.Unlock();
}
}
}
}
#endregion //Owner:撮影者 プロパティ
}
}
このように冗長な記述になりやすい Model のコードを Model.generated.cs というファイル名で自動生成してくれます
今回は Photo というクラスのみでしたが、ModelDefenitions.cs に複数のクラスとその属性定義を記述すれば、Model.generated.cs に記述した分のクラスが生成されるので、手早く
Model コードを作ることができるというわけです
属性記述は修正も簡単なので、データ構造が頻繁に変更されるような場合にもデグレードを起こさずに素早く対応できます
この仕組みを応用すれば、ViewModel、Model、WebAPI、DAO と各レイヤー用に T4 テンプレートを用意すれば、必要なエンティティクラスを1つの定義情報から一気に自動生成してデータ項目の整合性を保つといったこともできると思います
T4 テンプレートは記述が若干複雑になりやすく、仕掛けを作るまでは多少面倒ですが、一度できてしまえば冗長な記述を一気に吸収し開発生産性と保守性を大きく向上させてくれるので活用しないのはもったいないと思います!
Visual Studio Advent Calendar 2014 次回の記事の投稿者は NumAniCloud さんです!よろしくお願いしますー!