今更ですが、T4 Template

今更T4 Template弄ってみたけど、面白いですね(・∀・)
事の始まりは、前にT4MVCがうまく使えなかったこと。
T4MVCは単一のプロジェクトを想定した作りになっているので、Controllerが別プロジェクトにある場合には対応していなくて(´д`;)


っで、今後を考えて、自分でもテンプレートを弄れるようにしておこうと思って、遊んでみたり。
こういうのって、Hello World的なものを作っても面白くないので、それなりのお題を想定してテンプレートを作ってみました(・ω・)

本日のお題

お題としては、昔ながらの[クライアント]-[サービス]-[DAO]みたいな構成のアプリケーションを想定して。
DAOについて、interfaceを定義すれば実装を自動生成してくれるようなものを、T4 Templateを使ってやってみることにしました。
イメージとしては、AOPでDynamicProxyにより黒魔術的にコード生成していたものを、ソースレベルでのコード生成でやるという感じですね(・ω・)
ついでにサービスやDAOはMEFで解決する感じで。


プロジェクト構成はこんな感じ。


DAOフレームワーク仕様

コード生成するテスト用DAOフレームワークの仕様としては、下記のようなものを想定してみます。

// DAO定義
[AttributeUsage( AttributeTargets.Interface )]
public class DaoAttribute : Attribute
{
}
// SQLメソッド定義
[AttributeUsage( AttributeTargets.Method )]
public class ExecuteAttribute : Attribute
{
    public string Sql { get; set; }
}
// DAO本体のダミー実装
public static class DaoEngine
{
    public static void Execute(string sql)
    {
        System.Diagnostics.Debug.WriteLine( sql );
    }
}

DaoAttributeが付いたinterfaceの、ExecuteAttributeが付いたメソッドを自動生成する仕様で。
今回は基本的な実験だけなので、引数や戻り値の処理は省略して、メソッドシグネチャは戻り値void、引数無しのみに限定して(´д`)


自動生成するコードの中身も、DaoEngine.Execute()を呼び出すだけ。
生成されたコードを使ったプログラムで、ExecuteAttributeで指定したSQLがDaoEngine.Execute()によりデバッグ表示されたら確認OKということで(・ω・)

DAOインタフェース

っで、自動生成の元になるinterfaceをこんな風に定義。

[Dao]
public interface IHogeDao
{
    [Execute( Sql = "select * from Hoge" )]
    void GetList();

    [Execute( Sql = "insert into Hoge ( Id, Data ) values ( @id, @data )" )]
    void Insert();
}

このコードからT4 Templateを使って実装を生成してみます。

使う側

っで、自動生成されたコードを使う側はこんな感じで。

[Export]
public class HogeService
{
    [Import]
    public IHogeDao HogeDao { get; set; }

    public void DoService()
    {
        HogeDao.Insert();
        HogeDao.GetList();
    }
}
class Program
{
    static void Main(string[] args)
    {
        AssemblyCatalog catalog = new AssemblyCatalog( Assembly.GetExecutingAssembly() );
        CompositionContainer container = new CompositionContainer( catalog );

        HogeService service = container.GetExportedValue<HogeService>();
        service.DoService();
    }
}

MEFを使って、Program-HogeService-IHogeDao実装(自動生成)と処理を呼び出します。

DaoTemplate.tt

っということで、DAO実装を生成するためのT4 Templateのファイルを下記のように作成しました。
ソースの部分毎に簡単に解説。

<#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation" language="C#v3.5" debug="true" hostSpecific="true" #>
<#@ output extension=".cs" #>
<#@ assembly Name="System.dll" #>
<#@ assembly Name="System.Core.dll" #>
<#@ assembly name="Microsoft.VisualStudio.Shell.Interop.8.0" #>
<#@ assembly name="EnvDTE" #>
<#@ assembly name="EnvDTE80" #>
<#@ assembly name="VSLangProj" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #> 
<#@ import namespace="Microsoft.VisualStudio.Shell.Interop" #>
<#@ import namespace="EnvDTE" #>
<#@ import namespace="EnvDTE80" #>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>

まず最初はテンプレートで使うアセンブリ名前空間の定義。
標準的なものに加えて、Visual Studio関連のものを追加しています。


ちなみに、テンプレートの編集にはT4 Editorを使っていて、これを使うとテンプレートの中身が色分けされたり、インテリセンスが効いたりするんですが、Visual Studio関連のクラスについてはProfessional版じゃないとインテリセンス効かないよん、っと警告がでちゃいますね(´・ω・`)

<# PrepareDataToRender( this ); #>

次にテンプレートで使用するデータの準備について。
PrepareDataToRender()の詳細は後述しますが、テンプレートを作りやすいように事前にデータを準備しておきます。

// Generated by <#= T4FileName #>
using System;
using System.ComponentModel.Composition;
using System.Runtime.CompilerServices;
using Framework;

<# foreach( DaoInfo daoInfo in DaoList ) { #>
namespace <#= daoInfo.Namespace #>
{
    [CompilerGenerated]
    [Export( typeof(<#= daoInfo.Interface #>) )]
    public class <#= daoInfo.Interface #>Impl : <#= daoInfo.Interface #>
    {
<# foreach( ExecuteInfo executeInfo in daoInfo.Execute ) { #>
        public void <#= executeInfo.Name #>()
        {
            DaoEngine.Execute( "<#= executeInfo.Sql #>" );
        }

<# } #>
    }
}

<# } #>

っで、これがソースを生成するテンプレートの本体。
やっていることは、DaoInfoの数分DAO実装クラスを生成して、ExecuteInfoの数分のメソッドを定義をしています。
なお、T4FileName、DaoListといった変数はPrepareDataToRender()の中で準備しています。


DaoInfoクラスおよびExecuteInfoクラスについては以下のように定義していて。

<#+
// Daoインタフェースメタデータ
class DaoInfo
{
    public string Namespace { get; set; }
    public string Interface { get; set; }
    public List<ExecuteInfo> Execute { get; set; }
}

// Executeメソッドメタデータ
class ExecuteInfo
{
    public string Name { get; set; }
    public string Sql { get; set; }
}
#>

PrepareDataToRender()でこのデータを作成し、それを元にDAO実装のソースを生成しているわけです。
事前にメタデータを収集しておくのは、コード生成にテンプレート部とメタデータの取得が一緒だとグチャグチャするから。
あと、Visual Studio関連のクラスは、T4 EditorのFree版だとインテリセンスが効かないからという理由も(´д`;)


っで、以下が変数定義とPrepareDataToRender()の処理になります。

<#+
static TextTransformation TT;
static string T4FileName;
static DTE Dte;
static Project Project;
static List<DaoInfo> DaoList;

// レンダリング用データ作成
void PrepareDataToRender(TextTransformation tt)
{
    TT = tt;
    T4FileName = Path.GetFileName(Host.TemplateFile);
    
    var serviceProvider = Host as IServiceProvider;
    Dte = serviceProvider.GetService(typeof(SDTE)) as DTE;

    Project = GetProjectContainingT4File( Dte );
    
    DaoList = new List<DaoInfo>();

    foreach( ProjectItem projectItem in Project.ProjectItems )
    {
        ProcessDaos( projectItem );
    }
}

// Project取得
Project GetProjectContainingT4File(DTE dte)
{
    // .ttファイル
    ProjectItem projectItem = dte.Solution.FindProjectItem( Host.TemplateFile );
    
    if( projectItem.Document == null )
    {
        projectItem.Open( Constants.vsViewKindCode );
    }
    
    return projectItem.ContainingProject;
}

// Daoインタフェース取得
void ProcessDaos(ProjectItem projectItem)
{
    foreach( ProjectItem item in projectItem.ProjectItems )
    {
        ProcessDaos( item );
    }
    
    // ソースのみ
    if( projectItem.FileCodeModel != null )
    {
        foreach( CodeNamespace ns in projectItem.FileCodeModel.CodeElements.OfType<CodeNamespace>() )
        {
            // Interfaceのみ
            foreach( CodeInterface2 type in ns.Members.OfType<CodeInterface2>() )
            {
                for( int i = 1; i <= type.Attributes.Count; i++ )
                {
                    // DaoAttributeのみ
                    CodeAttribute2 attrib = (CodeAttribute2)type.Attributes.Item( i );
                    if( attrib.FullName == "Framework.DaoAttribute" )
                    {
                        DaoList.Add( CreateDaoInfo( type ) );
                    }
                }
            }    
        }
    }
}

// Daoメタデータ作成
DaoInfo CreateDaoInfo(CodeInterface2 type)
{
    DaoInfo daoInfo = new DaoInfo();
    daoInfo.Namespace = type.Namespace.FullName;
    daoInfo.Interface = type.Name;
    
    daoInfo.Execute = new List<ExecuteInfo>();
    foreach( CodeFunction2 method in type.Members.OfType<CodeFunction2>() )
    {
        ExecuteInfo executeInfo = new ExecuteInfo();
        executeInfo.Name = method.Name;
        for( int i = 1; i <= method.Attributes.Count; i++ )
        {
            CodeAttribute2 attrib = (CodeAttribute2)method.Attributes.Item( i );
            if( attrib.FullName == "Framework.ExecuteAttribute" )
            {
                executeInfo.Sql = GetSql( attrib.Value );            
            }
        }
        if( String.IsNullOrEmpty( executeInfo.Sql ) )
        {
            Error( "ExecuteAttribute sql is empty." );
        }
        daoInfo.Execute.Add( executeInfo );
    }
    
    return daoInfo;
}

// 手抜きSQL抽出(´д`;)
string GetSql(string value)
{
    int start = value.IndexOf( "\"" ) + 1;
    int end = value.LastIndexOf( "\"" );
    return value.Substring( start, end - start );
}
#>

PrepareDataToRender()の中身ですが、まずはT4 Template及びVisual Studio環境の情報を変数に保存しています。
その後、Project中のProjectItem(フォルダ、ファイル)に対してProcessDaos()を呼び出し、DAO実装の作成を行っています。


ProcessDaos()では再起処理とFileCodeModelの判定により、ソースファイルのみを処理の対象としています。
更にソース中の名前空間からinterfaceのみを対象として、CodeAttribute2によりDaoAttribute属性がついたもののみに対してCreateDaoInfo()を呼び出してメタデータを作成しています。


CreateDaoInfo()では指定されたinterfaceに対するDaoInfoメタデータの作成と、メソッドからExecuteAttributeが付いたもののみを対象としてExecuteInfoメタデータの作成をしています。

生成されるクラス

以上のテンプレートにより、下記のようなソースが生成できました(・∀・)

// Generated by DaoTemplate.tt
using System;
using System.ComponentModel.Composition;
using System.Runtime.CompilerServices;
using Framework;

namespace T4DaoWithMEF.Dao
{
    [CompilerGenerated]
    [Export( typeof(IHogeDao) )]
    public class IHogeDaoImpl : IHogeDao
    {
        public void GetList()
        {
            DaoEngine.Execute( "select * from Hoge" );
        }

        public void Insert()
        {
            DaoEngine.Execute( "insert into Hoge ( Id, Data ) values ( @id, @data )" );
        }

    }
}

っで、プログラムの実行結果もこんな感じで、MEFでインジェクションされたクラスを使って処理が実行できている事を確認できまスタヽ(´ー`)ノ

insert into Hoge ( Id, Data ) values ( @id, @data )
select * from Hoge

評価

っで、評価。
T4 Templateの使い方をわかったんですが、このレベルの生成だと嬉しくはないですね(´д`;)*1
っというか、Attributeをテンプレートで処理するっていうのは考え方が間違ってますよね(´д`;;)


T4 Templateを有効利用する方法としては、やっぱり定型の処理実装とかですかね(・∀・)?
データベース関連で、DBのメタデータからデータアクセス&容れ物クラスを作るとかがありがちなアイディアですが。
でも、LINQ to SQL使っているぶんにはsqlmetalでいいんだけど。
あとは、T4MVCみたいに、Type Safeな処理を書くために「名前」の定数を自動生成するとかですが。


なかなか良いアイディアを思いつかなかったりして(・∀・;)

*1:まあ、サンプルレベルというのもあるんだけど(´・ω・`)