kazuk は null に触れてしまった

C# / .NET 系技術ネタ縛りでお送りしております

カテゴリーアーカイブ: T4

Microsoft Code Digger をさっそくハックした


昨夜リリースされたばかりの Microsoft Code Digger をさっそくハックしてみました。

明け方の段階ではつかえねーコレだったのですが、ハックによってそこそこ使えるようになった感じです。

何ができる物ですか?

簡単に言えば Pex の低機能版です。一メソッドを選択し、コンテキストメニューから Generate Inputs / Outputs Table を行うとPexでの動作パスの列挙および、その動作パスに入るパラメータ値、そのパラメータでの呼び出し結果がリストアップされます。

    public class Class1
    {
        public int Test( int x,int y)
        {
            checked
            {
                return x/y;
            }
        }
    }

というTestAddを Generate Inputs/Outputs すると以下のように表示されます。

image

UI上ではそれだけです。これ以上の何もできません。

結果が気に入らなければコードを直せば良い、気に入ったならそのコードで良いんでしょ。別にすることないよねって鼻くそをホジってる感じです。

何ができない物ですか?

Code Digger 単体ではこのコードが将来に渡って同一の結果を返し続けるように保障する事はできません。

この一覧を元に単体テストを作って、ビルドサーバでそれを実行するようにすればそれはきっと実現する事ができるでしょう。Pexにはそれは備わっていましたが Code Digger には含まれません。

実際の所として現状追認のテストコードが一杯できるだけで、Pexでのテストコード生成は決して便利とは言いきれなかったのですが、それにしてもバッサリ切ってしまわれるとちょっとアレです。

カッとなってハックした

というわけで、Code Digger の出力をちょっと眺めてみたらテストコード自体は生成しているという事が解ったので、それを元に C# での単体テストコードを再構成する T4 テンプレートを書いてみました。

現物ファイルは gist にあります。

https://gist.github.com/kazuk/5449781

ファイルをダウンロードしたうえで Code Digger と組み合わせて使ってみましょう。

ポータブルクラスライブラリと単体テストのプロジェクトを作ります。

image

デフォルト名でビシバシひどいですが、こんなもんです。

足し算では単調すぎるので、割り算でやってみましょう。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace PortableClassLibrary1
{
    public class Class1
    {
        public int Test( int x,int y)
        {
            checked
            {
                return x/y;
            }
        }
    }
}

というコードに対して

image

という結果が得られました。

さっきの gist から取ってきたttを単体テストプロジェクトに突っ込みます。

最初の <#@ assembly #>の行を

<#@ assembly name=”$(SolutionDir)\PortableClassLibrary1\bin\Debug\PortableClassLibrary1.dll”#>

と書き換えて保存します。

普通に C# ソースが吐かれますが、一部エラーとなっているはずで、TODOコメントがいくらか出ていると思います。

// info PexFrameworkDir = C:\Users\kazuk\AppData\Local\Microsoft\VisualStudio\11.0\Extensions\z2qe4t0q.wnk
// TODO: add reference Microsoft.Pex.Framework.dll to this Project. path shown previous line

TODOの二つ目が上記内容です。どこにインストールされるかはちょっと予想がつかないので、パスが表示されています。ここにある Microsoft.Pex.Framework.dll を参照設定してください。(ソースコードを共有している場合に人様が自分のAppData\Local を参照できないとかで困らないようにソリューション内の適当な場所にコピーする等気は使って下さいね。)

これでエラーは消えるはずです。

もう一つのTODOである名前空間まで調整すれば特にもうする事はありません。

テストは生成されていますので、実行してみましょう。

image

もともとCode Digger で失敗したよと表示されているテストは失敗するように生成されました。

テストをいじったのにT4 から再生成されては意味がないので、ソースコードをまっとうなファイル名を付け替えて保存すれば完了です。

最後に例外で失敗してる所とかをそれを望むなら PexRaisedExceptionのtypeof通りに ExpectedException で想定通りと言い聞かせればいいでしょう。

注意事項

複数の Code Digger の結果が残ってると一気に生成するので生成メソッドが重複する事があります。bin\Debug配下にreportsとして結果は出ていますので、不要な物は適宜削除するか、さっくりデバッグしたい時などは名前空間を変える等で重複をごまかしておけば良いんじゃないかな。

まとめ

普通に使うにはちょっと機能不足な気もするけど、機能が無いならハックすれば良いじゃないで何とかなってみました。

Pex は楽しいツールなんでうまく使って下さい。ではでは!ハッピーな開発を!

広告

MVC と Code Contracts と DataAnnotations


MVC でリポジトリパターン?サービスインターフェース?を Code Contracts で堅くいこうとすると、こんな恰好になりますよね?

[ContractClass( typeof( RepositoryContracts ) )]
public interface IRepository
{
    Data GetByName( string name );
}

[ContractClassFor( typeof( IRepository ) )]
abstract class RepositoryContracts : IRepository
{
    public Data GetByName( string name )
    {
        Contract.Require( name!=null );
    }
}

これをやればリポジトリを無茶なパラメータで呼ぶような事は契約によって守られるわけで素晴らしいんだけど。

namespace Application.Models
{
    public class QueryViewModel
    {
        [Require]
        public string Name { get; set; }
    }
}

なViewModel を元にコントローラで検索するこんなコード

[HttpPost]
public ActionResult Query( QueryViewModel model )
{
    if( ModelState.IsValid )
    {
        var data = _repository.GetByName( model.Name );
        return View( data );
    }
}

IsValid 確認したって、code contracts にとってはそんなこと知ったことなくで unproven 祭り。

この unproven を解消する為に Assume をここに書き込むのは避けたい、Model 側の属性で制御してる事柄を Controller に書き込んでいるも同然で、Model を仕様変更した途端に Assume は嘘に変わる可能性が無いわけではないし、そもそも DataAnnotations でやっている検証で保障してる内容を再確認するコードだし二重記述な気がして嫌だというわけ。

Model にこんなメソッドを書く

public void EnsuresAssumeAsValid( )
{
    Contract.Ensures( Name!=null );
    Contract.Assume( Name!=null );
}

そうすると、さっきのコントローラーでは model.EnsuresAssumeAsValid() と ModelState.IsValid の確認後に呼んであげれば Name が null じゃないという認識になる。

んで DataAnnotations での検証結果が反映されるようにEnsures/Assume するメソッドを書く、DataAnnotations での設定に従ってという仕事が残る。

形を変えての繰り返し作業ってメンドイよね、飽きやすいし、けどミスは許されなくて、属性書き換えたらちゃんと直さないと Code Contracts が絵に描いた餅になる。

でだ、そこで T4 だ

リフレクションで舐めるから Models は別アセンブリになる必要がある、まぁ、MVC でアプリケーション作ったら Models は別アセンブリにするのはレイヤ分離上も結構良くやる事なんで別に構わんだろう。

image

という感じで ASP.NET MVC3 のデフォルトの Models にちゃんと Contract を意識したコードを吐いてるのが以下のT4コード、これで ModelState.IsValid だったら model.EnsureAssumeAsValid() ってやれば DataContracts に従った Ensures/Assumeがされる。

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.ComponentModel.DataAnnotations" #>
<#@ assembly name="$(SolutionDir)\MvcApplication2.Models\bin\Debug\MvcApplication2.Models.dll" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
<#
    // ここで属性に対する Ensures / Assume を定義します
    AttributeTypeMap.Add(
        typeof( System.ComponentModel.DataAnnotations.RequiredAttribute ),
        new EnsuresAssumeActions { 
            Ensures= (attrData, propInfo) => {#>
            Contract.Ensures( item.<#=propInfo.Name#> != null );
<#},
            Assume= (attrData, propInfo) => {#>
            Contract.Assume( item.<#=propInfo.Name#> != null );
<#}
        }
    );
#>
using System.Diagnostics.Contracts;

namespace MvcApplication2
{
    public static class ContractHelper
    {
<# 
// モデル型をちゃんと抽出する様に直してね

var typeQuery = from asm in AppDomain.CurrentDomain.GetAssemblies() where asm.GetName().Name=="MvcApplication2.Models" from t in asm.GetTypes() select t; foreach( Type t in typeQuery ) {#> // <#=t.Name#> public static void EnsureAssumeAsValid( this <#=ClassName(t)#> item ) { <# foreach( var p in t.GetProperties() ) { #> <# foreach( var attr in p.GetCustomAttributesData() ) { EnsuresAssumeActions actions; if( AttributeTypeMap.TryGetValue( AttributeType(attr), out actions ) ) { actions.Ensures( attr, p ); } } #> <# } #> <# foreach( var p in t.GetProperties() ) { #> <# foreach( var attr in p.GetCustomAttributesData() ) { EnsuresAssumeActions actions; if( AttributeTypeMap.TryGetValue( AttributeType(attr), out actions ) ) { actions.Assume( attr, p ); } } #> <# } #> } <# } #> } } <#+ class EnsuresAssumeActions { public Action< CustomAttributeData, PropertyInfo > Ensures; public Action< CustomAttributeData, PropertyInfo > Assume; } Dictionary<Type, EnsuresAssumeActions> AttributeTypeMap = new Dictionary<Type, EnsuresAssumeActions>(); public string ClassName( Type t ) { return t.FullName; } public Type AttributeType( CustomAttributeData attrData ) { return attrData.Constructor.DeclaringType; } public string AttributeTypeName( Type t ) { string s =t.Name; s = s.Substring( 0, s.Length- "Attribute".Length); return s; } #>

Requires にしか変換パターン書いてないけど、必要に応じて足してくれればいいよね。

これで Unproven 祭りに Assume 祭りの応戦をする必要はなくなったね! code-contracs 活用して堅いサービス書いてね!、それでは see you!

Implementing Visual Studio Custom Tool step by step


環境の準備

というわけで Visual Studio でのカスタムツールの実装方法です。

まず最初に Visual Studio 拡張のSDKが必要になりますので、ダウンロードしてインストールしてください。 Extending Visual Studio から Visual Studio 2010 SDK

で、非常に定型コードの多い、カスタマイズの必要のない部分が多いので、Razor Preprocessor のソースを持ってると非常に楽です(っていうか、とってきてそのままプロジェクトに入れて下さい的解説が多く出ます)ので zip は拾って展開しておいてください。

プロジェクトの作成

Visual Studio 2010 SDK をインストールすると、Extensibility 配下に VS Package プロジェクトが表示され、Visual Studio 拡張のパッケージを作成する事ができます、この VS Package プロジェクトにカスタムツールを実装する事になります。

プロジェクトを作成したらまず vsix のマニフェスト編集になります。ここで一番重要なのは統合対象のVSエディションの選択です、デフォルトでは Professional のみになってますので必要な統合先を選択してください。

image image

プロジェクトビルドオプションの設定

Visual Studio 2010 では基本的に MEF による拡張になっている為、VS Package はデフォルトで COM 公開になっていません。これを有効にします。プロジェクトのプロパティでアセンブリ情報をクリックし、アセンブリをCOM参照可能にします。

image image

参照設定

VS Package の標準参照設定では Custom Tool を実装する上でのインターフェース定義等いくらか不足しますので、これを参照設定を行い取り込みます。

image

追加されている主な物はEnvDTE80, Microsoft.VisualStudio.OLE.Interop, Microsoft.VisualStudio.Shell.Inteop, VSLangProj, VSLangProj80ぐらいですね。その他の System.Web 配下とかは今回 Razor のコード生成が必要だったのでくっついてるだけで、Custom Toolの実装に必要というわけではありません。Visual Studio の NoPIA のデフォルト動作が VS 連携がらみのシナリオとバッティングをおこしビルド時に警告が出るようになると思います。VS連携系のアセンブリの参照設定時には「相互運用機能の埋め込み」はFalseを指定してください。

image 

pkgdef 属性定義宣言

カスタムツールは COMモジュールであり、VS からレジストリを介して参照されますので、パッケージのインストール時に pkgdef によるレジストリ設定を行う必要があります。このレジストリ定義をvsixインストーラーが解釈可能な形で渡すためにアセンブリ内のクラス宣言に付与した属性から抽出する仕組みが Visual Studio SDKには含まれています。RegistrationAttribute(Microsoft.VisualStudio.Shell.RegistrationAttribute クラス) はあまりに汎用過ぎてモジュール定義的な内容しか出力できないので、この属性定義の派生を作成します。CodeGeneratorRegistrationAttributeクラスがこの実装になります。カスタムツールのpkgdefを生成する上でいじるところは特にないのでそのままソースをプロジェクトに組み込んでください。

IVsSingleFileGenerator の実装

IVsSingleFileGenerator を実装します、COMインターフェースなんでメモリ管理面等気を使わなければいけない部分が幾つかあるのですが、必要十分な実装が BaseCodeGenerator.cs にありますのでこれを取り込みます。

同じく IObjectWithSiteも実装しなければならないのですが BaseCodeGeneratorWithSite.cs に必要十分な実装があります、この実装を取り込む事で Visual Studio のプロジェクトシステム内のサービス提供モデルに組み込まれる事ができ、結果的に VSLangProj等のインターフェースが利用可能になります。(この時にエラーメッセージの文字列リソースとして Strings.resxが必要になります。 resx を取り込んでカスタムツールに ResXFileCodeGenerator を指定してください)

最終的に Custom Tool の実装はここで準備した BaseCodeGeneratorWithSite から派生されます。

カスタムツール本体の実装

BaseCodeGeneratorWithSiteの継承クラスを作成します。実装しなければならないメソッドは GenerateCode だけです。GenerateCodeのinputFileContentパラメータに文字列でカスタムツールが設定されているファイルの内容がもらえますので、これを元にツールの出力を byte[] で返す事になります。 byte[] で返すので出力はテキストでなくてイメージファイルだったりなんかのバイナリでも構いません。逆にテキストを返す場合にはちゃんと出力の文字コードへのエンコード処理を記述しなければなりません。
続いて継承クラスを CodeGeneratorRegistrationAttribute で属性修飾します。同時に ComVisibleをtrue、 Guid属性、ProvideObject 属性で実体型を示す必要があります。

    [ComVisible(true)]
    [Guid("0E28FE58-1755-465E-A2D9-8B4AFD875742")]
    [CodeGeneratorRegistration(typeof(className), 
        "説明", 
        vsContextGuids.vsContextGuidVCSProject, 
        GeneratesDesignTimeSource = true)]
    [ProvideObject(typeof(className))]
    public class className : BaseCodeGeneratorWithSite
    {
        protected override byte[] GenerateCode(string inputFileContent) 
        {
            // ここでカスタムツール固有の実装をします。
        }
    }

実装しなければならないメソッドは単純に言えば string->byte[] ですので、単体テストプロジェクトから色々呼び出してテストしておくと最終工程で VS デバッガの中にVS読み込んでの確認をする必要性が減ってが楽になるんじゃないかなーとは思います。

デバッグと確認

単純にBuildしてデバッグしようとしても必要なレジストリ登録とか vsixインストール時に行われるべき事がされてない形になります、bin\Debug配下にvsixが出力されていますので vsix をインストールしてからデバッグ実行します。

デバッガから抜ける前に機能拡張マネージャからアンインストールを叩いておいたほうが良いでしょう。(もう一度 vsix インストールしようとしてダブルクリックすると「すでにインストールされています」で断られますので)

プロジェクトの種類は何でも構わないのでカスタムツールに渡すファイルを持つためのプロジェクトを作り、テキストファイルを作ります。カスタムツールを作成したカスタムツール名に設定してカスタムツールを実行でカスタムツールが実行されます。ブレークポイントを貼っていればブレークされるはずなので好きなだけデバッグしてください。(ただし、デバッガ内でVS起動するとCore2 Duo 2.8GHzでも起動するまでに30秒以上かかりますので、最後の統合レベルのデバッグ以外はデバッガ実行でのデバッグはおすすめできません。)

まとめ

実装そのものは部品がそろってれば比較的簡単でした。躓きどころはMEFでなくてCOMだっていう点、これについての注意事項はこの文書にすべて盛り込んだつもりです。MS提供のほとんど使える部品がそろってるのでそれをベースに淡々と組みましょう。

T4が AppDomainの分離とかで切り離されるとかの絡みで触れないインターフェース、サービスその他があるのに比べてこっちは比較的なんでも行ける感があります。T4のPreProcessor使って生成周りのコード作って呼ぶだけなら生成周りの実装も簡単と思いますので、T4のヘビーユーザーで深いVS Integrationを求める人は挑戦する価値はある素材だと思います。

T4でサムネイル画像を生成する


Webの制作っていうと何かと画像処理って出てくるわけでございます。

一括処理とかムニャムニャ考えないと日がな一日ツールをポチポチする羽目になってタンポポ中毒になってしまいます。

てな訳で T4 から GDI+ で一括処理だ!

とりあえずサムネイルを作るだけならこんな感じ。もちろんの事ながら画像を収める img タグとかも同時に吐かせようと思えば余裕!

 

   1:  <#@ template debug="false" hostspecific="true"  language="C#" #>
   2:  <#@ output extension=".txt" #>
   3:  <#@ assembly name="EnvDTE" #>
   4:  <#@ assembly name="System.Core"#>
   5:  <#@ assembly name="System.Drawing" #>
   6:  <#@ import namespace="System.Drawing"#>
   7:  <#@ import namespace="System.Drawing.Drawing2D"#>
   8:  <#@ import namespace="System.Drawing.Imaging"#>
   9:  <#@ import namespace="System.IO"#>
  10:  <#@ import namespace="System.Collections.Generic"#>
  11:  <# 
  12:      var dir= Path.GetDirectoryName( Host.TemplateFile );
  13:      var thumbImageDir = Path.Combine( Path.GetDirectoryName(dir), "thumbs");
  14:      double maxWidth = 320;
  15:   #>
  16:      Source dir:    <#= dir #>
  17:      Target dir: <#= thumbImageDir #>
  18:  <#
  19:   
  20:      foreach( var pngFile in Directory.GetFiles( dir,"*.png" ) )
  21:      {
  22:  #>
  23:  <#=pngFile#> を変換中
  24:  <#
  25:          using( Image img = Image.FromFile( Path.Combine( dir, pngFile ) ) )
  26:          {
  27:              Size sizeImg = img.Size;
  28:              if( sizeImg.Width>maxWidth ) 
  29:              {
  30:                  double factor = maxWidth / (double)sizeImg.Width ;
  31:                  sizeImg.Width = (int) (sizeImg.Width * factor);
  32:                  sizeImg.Height =(int) (sizeImg.Height * factor);
  33:  #>
  34:      original width:<#=img.Width#>
  35:      original height:<#=img.Height#>
  36:      factor: <#=factor#>
  37:      width: <#=sizeImg.Width#>
  38:      height: <#=sizeImg.Height#>
  39:  <#            
  40:              }
  41:              using( Image small = new Bitmap( sizeImg.Width, sizeImg.Height, img.PixelFormat ) )
  42:              {
  43:                  using( Graphics g = Graphics.FromImage( small ) )
  44:                  {
  45:                      g.CompositingQuality = CompositingQuality.HighQuality;
  46:                      g.InterpolationMode =InterpolationMode.HighQualityBicubic;
  47:                      g.DrawImage( img,0,0, sizeImg.Width, sizeImg.Height );
  48:                  }
  49:                  string savePath =Path.Combine(thumbImageDir, Path.GetFileName( pngFile ) );
  50:  #>
  51:      saveTo:<#=savePath#>
  52:  <#
  53:                  small.Save( savePath,ImageFormat.Png );
  54:              }
  55:          }
  56:      }
  57:  #>

 

一応ちゃんとしっかり using を使ってるけど、へくってると画像の大きさとかによってはVSが巻き添えになる可能性もなきにしもあらずなんで、usingはしっかりしようね!

dbml のAssociationの逆参照を得る


先日のttソースの貼り付けで酷い目にあったから。コード貼り付けのテストだったりする。

    XElement GetReverseAssociation( XElement assoc )
    {
        var typeElement = (from db in dbmlDoc.Elements( dbmlNs+"Database" )
                          from table in db.Elements( dbmlNs+"Table" )
                          from type in table.Elements( dbmlNs+"Type" )
                          where type.Attribute("Name").Value==assoc.Attribute("Type").Value
                          select type).Single();
        return (from revAssoc in typeElement.Elements( dbmlNs+"Association" )
                where revAssoc.Attribute("Name").Value==assoc.Attribute("Name").Value
                select revAssoc ).Single();
    }

 

って感じで dbml (LINQ to SQL のメタデータ)から参照に対する逆参照を得られる。

    XDocument dbmlDoc;
    XNamespace dbmlNs;
をフィールドとして用意する事、それぞれ以下の様な初期化をする必要がある。
    dbmlNs = "http://schemas.microsoft.com/linqtosql/dbml/2007";
    dbmlDoc = XDocument.Load( dbmlFilePath );

パラメータの XElement assoc には dbml内の Association 要素を指す XElement を渡してね!

テストだから崩れても泣かない。

POCO Entities with LINQ to SQL


dbml ファイルはノーマルなXMLなんで、T4を介して LINQ to XML で舐めて生成してあげればおっけー

下のttで POCO エンティティとのコピーコンストラクタを LINQ to SQL クラスに partial part で足せるよっと。

dbmlで定義されたクラスに基づいてPOCOクラスを作るとか発展は皆さまにお任せ。

 

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core.dll" #>
<#@ assembly name="System.Xml.dll" #>
<#@ assembly name="System.Xml.Linq.dll" #>
<#@ import namespace="System.Collections.Generic"#>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Xml.Linq" #>
<#@ import namespace="System.Linq" #> <#
/************************************** configurations */
string dbmlFilePath = Path.Combine( Path.GetDirectoryName( Host.TemplateFile ), "Northwnd.dbml" );

PocoEntityTypeNames = new Dictionary<string,string> {
/* example
{ "L2SClassName", "PocoClassName" } */
};
FallbackPocoEntityTypeName = s => s+"Entity";

/************************************** implementation */
XNamespace ns = "http://schemas.microsoft.com/linqtosql/dbml/2007&quot;;
var dbmlDoc = XDocument.Load( dbmlFilePath );
var tables =
from db in dbmlDoc.Elements( ns+"Database" )
from table in db.Elements( ns+"Table" )
select table;

Generate( tables, table=>{
var typeElement =table.Element(ns+"Type");
var typeNameAttr = typeElement.Attribute("Name");
#>
public partial class <#=typeNameAttr.Value#>
{
#region CopyCtor
public <#=typeNameAttr.Value#>( <#=GetPocoEntityTypeName(typeNameAttr.Value)#> source )
{
<# Generate( typeElement.Elements(ns+"Column"), col=>{#>
this.<#=col.Attribute("Name").Value#> = source.<#=col.Attribute("Name").Value#>;
<#});
#>
}
#endregion
}
<#});

#>
<#+
Dictionary<string,string> PocoEntityTypeNames;
Func<string,string> FallbackPocoEntityTypeName;

string GetPocoEntityTypeName( string typeName )
{
string pocoTypeName;
if( PocoEntityTypeNames.TryGetValue( typeName, out pocoTypeName ) )
{
return pocoTypeName;
}
return FallbackPocoEntityTypeName( typeName );
}

void Generate<T>( IEnumerable<T> collection, Action<T> generateAction )
{
foreach( T item in collection )
{
generateAction( item );
}
}
#>

T4からの脱獄キットリリースしました


つい先日、「良く訓練されたT4使いは…」なんて事を書いてたりしましたが…
 
絶望していた ToStringHelper に死亡宣告を与える事に成功しましたので公開致します。
 
 
ReadMe.txt をべたっと貼り付け。英語の添削できるひとの突っ込み希望です。
 
ReadMe.txt に書いてある事ですけど、やっている事は T4 が作る .cs の中で ToStringHelper に対してもっとマットウな実装を作ってぶち当てる。
.cs に書いてあるコードと、参照アセンブリにあるコードでは .cs に書いてある方が優先されて使われるので結果としてToStringHelperのアホさかげんに付き合う必要なんてナッシングって事です。
 
 
	this is a readme for using T4 Jail Braking Kit.
	(this readme is Generated by ReadMe.tt on this folder)

1.	What Is Jail.

	T4 is output to <#=value #> to 
	
	in TextTemplatingFileGenerator

		Write( Microsoft.VisualStudio.TextTemplating.ToStringHelper.ToStringWithCulture( value ) )

		oops, static method call, can't be change to anything.

	in TextTemplatingFilePreprocessor

		Write( ToStringHelper.ToStringWithCulture( value ) )

	this ToStringHelper.ToStringWithCulture is so STUPID and anything to do replace.
	( ToStringHelper not partial and,ToStringWithCulture is not virtual )

		path the null, throws ArgumentNullException, I write code!, write be "null"!
		path the boolean, "True", "False", I don't know VB.
		path the List, OK, you recommend <#@ output extension=".il" #>!

	I do everytime grep and replace 'Write( ToStringHelper.ToStringWithCulture(' to runs 
	after TextTemplatingFilePreprocessor, to runs after, after,

	In result, I do brake the Jail, now!

2.	Detail of Jail Brake.

2.1.	in TextTemplatingFileGenerator,

	 Kill the stupid ToStringHelper with type definition confrict, 
	in type definition confircted , C# is prior .cs defined as referenced assembly defined.
	 That's raise warning, but success fully compiled and run.

	 '<#+ code #>' contexted, namespace with 'Microsoft.VisualStudio.TextTemplating(random value)'
	and TextTransformation class.

	<#+
	} // exit class
	} // exit namespace

	namespace Microsoft.VisualStudio.TextTemplating // target stay in close!, ready to fire...
	{
		pubic partial class ToStringHelper // Fire!, All belettes to be use! collectry kill that!
		{
			// implement the more smart guy

	but, i can't return original context because namespace is named by rundom value,
	T4 is last of '<#+' and '#>' renders '}' for exit class,'}' for exit namespace.
	
	In result, i stay in my ToStringHelper class contxt, T4 is closing by default,
	and can be exetend to my ToStringHelper implementation by after include .tt's.

	( dont be afraid, your tt's <#+ #> section is renders out after complete
	TextTransformation ,see Appendix B)

	Jail Braking require's only 2 line ,write below  in last of your tt's head 
	of directivs (see this file's top)

	<#@ include file="BrakeJail.ttlib" #>
	<# Microsoft.VisualStudio.TextTemplating.ToStringHelper.Initialize(this); #>
	
	 i implements ToStringHelper for you, this is extensible and having 
	Dictionary> your custom type formatters to add it.
	 this implementation is little bit complex, 
	
	because 
	  avoid boxing. if you int formatter added, int value was used by just
	 int, no any type casting and not be place to object.
 	  reflection result cashe implemented transparently a expression compiled 
	 result cache,
	
	perfomance is justice!

	usage in example 
	on head of file <#@ import namespace="Microsoft.VisualStudio.TextTemplating" #>
	<# ToStringHelper.AddFormatter( (bool b)=>b?"true":"false" ); #>
	<#= 0==1 #>

	result:
		false

	second choice, you handle event of ToStringHelper.ResolveFormatter

	this choice can be to do that, if type is array, create array formatter etc...

	in example, List formatter is shown below:

	<#  
		EventHandler handler =
		(src, e) =>
		{
			Type t = e.TypeForResolveFormatter;
			if( t.IsGenericType && t.GetGenericTypeDefinition()==typeof(List<>) )
			{
				e.SetFormatter( t, GetListFormatter(t) );
			}
		};

		ToStringHelper.ResolveFormatter+=handler;
	#>
	and bottom of this file implement it:
	<#+
		public static string ListFormatter( List list )
		{
			return "new List<"+typeof(T).FullName+"> {" +
					string.Join( ",", (from item in list 
									select ToStringHelper.ToStringWithCulture( item ) ) )
					+ "}" ;
		}

		public Delegate GetListFormatter( Type listType )
		{
			Type[] genArgs = listType.GetGenericArguments();
			var mi = this.GetType()
				.GetMethods()
				.Single( m=>m.Name=="ListFormatter" )
				.MakeGenericMethod( genArgs[0] );
			var listParam = Expression.Parameter( listType, "list" );
			var exp = Expression.Lambda( 
				Expression.Call( mi, listParam ),
				listParam );
			return exp.Compile();
		}
	#>
	
	I do it:
	<#= new List { 1,2,3 }#>
	result:
	new List {1,2,3}

	Note:
		reflection to Generic Methods pattern.

		 method info of Method 
			-> MakeGenericMethod 
				-> Exp.Lambda( exp.Call )
					-> .Compile()
						-> .Invoke()

		 is standard of todays reflection to Generic Methods pattern.

		
		Delegate is sometime used Unknown type Lambda in code.

		if you use Delegate.Method.GetParameters()
		index 0 was sometime not real parameter thats ParameterType is
			System.Runtime.CompilerServices.ExecutionScope (in mscorlib 2.0),
			System.Runtime.CompilerServices.Closure (is mscorlib 4.0), 
		
		in Delegate.Target==null
			GetParameters results parameters from index 0.
		in Delegate.Target!=null
			GetParameters results parameters from index 1,
				index 0 is Target's type.
		


2.2	in TextTemplatingFilePreprocessor

	I writing code now! may be as soon complete and show it.

----------------
Appendix. A. 
	My .tt nameing rule.

	.tt		designed for run with CustomTool 
	.ttinc	include file contains '<# #>' or else normal text.
			because output is created <#@ include #> only.
	.ttlib	include file but only contents '<#+ #>' sections 
	        because no output <#@ include #> only.

	Note : T4's rule of .tt and included files output ordering
		(not MS specified and no source, 
		 this is only my trying some tests and results seems to be it.
		 if MS specified it, send link to @kazuk on twitter)

	example:

	<#@ include filename="file1"#>
	<#@ include filename="file2"#>
	<#@ include filename="file3"#>
	my tt contents of normal text and '<# #>' sections
	my tt <#+ #> sections

	output does below order.

	1. nomal text and '<# #>' sections from file1
	2. nomal text and '<# #>' sections from file2
	3. nomal text and '<# #>' sections from file3
	4. my tt contents of normal text and '<# #>' sections
	5. my tt <#+ #> sections
	6. <#+ #> sections from file1
	7. <#+ #> sections from file2
	8. <#+ #> sections from file3

Appendix B.
	solution for 'Only one chance for override Dispose(bool) problem'

	on your include list
	file1.ttlib  override Dispose(bool) and appeed expencive object property for tt
	file2.ttlib  override Dispose(bool) and appeed expencive object property for tt 
	... thats can't be

	If you trade me the 'only on chance for override Dispose(bool)'
	include 'DisposeImplement.ttlib' on this archive Utilities folder.

	this has two protected field 
	IList ExpenciveObjects 
	IList> ExpenciveObjectRetrievers

	and override Dispose(bool) run's Dispose call of all ExpenciveObjects 
	and call's ExpenciveObjectRetrievers,
	 if Func returns non null, call the Dispose to it.

	your tt's add object to ExpenciveObjects or 
	ExpenciveObjectRetrievers.Add( () => this.MyProperty )

	Note:

	I know 'only one chance for implement class initializer problem' and
	'only one chance for implement defalut constructor problem' ,
	'only one chance for implement override Initialize problem' but i dou't have
	utilizable solution for it. let me it.

	( I will be implement 'override Initialize' in this Jail Brake kit,
	but this is only one chance and by case broke your tt's, I do not it,
	remember call Initialize method on my ToStringHelper on your file head!!)

 

csproj.user を作るための T4 テンプレート


スイカドライブに BuildOptions.tt なるものを投下いたしましたのでご案内。
(またリンクを Spaces が消したら Blog の移転を考えるなう)
 
 デバッグモードを持っていて、デバッグモード時には msbuild を実行してログを取って BuildOptions.txt に吐き出してくれますので、編集して保存(T4 では save is execute なのだよ)の繰り返しでパタパタといじっていけるはずです。デバッグモードでない場合にはこんなのを csproj.user に吐いたよと BuildOptions.txt に吐き出します。
 
こいつに色々と盛り込まれている 「よく訓練されたT4使いになる為のあれこれ」 をご紹介しましょう。
 
  1. よく訓練されたT4使いは 「何を元に作るか」 「何を作るか」 だけを考える。

    <# Generate( (何を元に作るか), (o)=>{#> 何を作るか <#} ); #> と書く。

    ファイルの最後に飛んで以下のように書く。

    <#+
    void Generate<T>( IEnumerable<T> seq, Action<T> action )
    {
        foreach( var item in seq ) { action(item) };
    }
    #>

    ポイントは T4 のコードブロック中で (value)=>{#> むにゃむにゃ <#} は立派な Action<T> な statement lambda だという事です。

    何を元に作るかはきっと from … select になるでしょう。 何を作るかの中では <#=o.Property#> で値を出力する事ができます。

  2. よく訓練されたT4使いは TextTemplate クラスの外にコードを置ける

    さて、<#@ include filename=""> でこんなファイルを取り込むとします。

    <#+ } 

    // ここはクラスの外!

    public partial class 読み込み元のTTのクラス名 { #>

    1で作った Generate メソッドを IEnumerable<T> に対する拡張メソッドにするとかそんな事しちゃいけません。ダメ、絶対。

  3. よく訓練されたT4使いは T4 の Write を殺す

    今回はやってませんけど(出力はxmlとtextだし、改行の調整とか必要無いから)、習慣としてファイル末尾で

    <#+
    public new void Write( string value )
    {
        GenerationEnvironment.Append( value.Replace( Environment.NewLine, Environment.NewLine+CurrentIndent );
    }
    #>

    するほうが良いです。
    VS2010 使いの人は CustomTool を GeneratorからPreprocessor に変えると cs ファイルを得る事ができますが、 Base のWrite は色々ごちゃごちゃやっていて結果を想像するのが大変だし、なんだかなーです。

  4. よく訓練された T4 使いは ToStringHelper に絶望している。

    3と似たりよったりですが、partial がついてなかったり、ToStringWithCulture に virtual をMSの中の人が付け忘れたせいで bool は TrueとかFalse とか出るし、null に対して "null" と文字列を出すとかもできません。なんかオブジェクトだすとクラス名だけになっちゃったり。List<T>とか出すと <#@ output extention=".il" #> がお勧めですか!って文字列になります。

  5. よく訓練された T4 使いは IFormatProvider にも絶望している。

    System.Boolean.ToString( IFormatProvider )

    解説


    provider パラメータは予約されています。このパラメータは、このメソッドの実行には関与しません。

  6. (Added 2010/8/15) 良く訓練された T4 使いは脱獄する

    脱獄キットをリリースしました

  7. よく訓練された T4 使いは Preprocessor かけてから Write(ToStringHelper.ToStringWithCulture( を置換する

    (もうT4じゃなくなってるという突っ込みは許可しない)

  8. よく訓練された T4 使いは TextTemplatingFileGenerator をレジストリ検索して…

    「HOWTO: Visual Studio のカスタムツールの作り方 (C#) 」ってMSDNドキュメントとか、KBどっかに無いもんか?

まぁ、後半は冗談交じりですがこんな感じです。

VS2010 への移行がらみで CLR2 と CLR4 の両方をターゲットにビルドしたいとか色々ありげな今日この頃、皆様の助けとなる tt であるかなーとかいう事で。TTファイルは以下

( やっぱり消えた、僕の SkyDrive の public にあるからどーぞ )

以下、余談。

そうそう、CLR 4.0 (mscorlib 4.0) で string は真に imuttable になりましたねー

CLR2 の間は「string は imuttable だから」とか書いてあると鼻で笑ってしまう僕だったのですが、もうそんな事も無くなって良いことです。

え? って思ったアナタは mscorlib 2.0 の StringBuilder の Append にデバッガで step-in してみる事をお勧めします。

CLR2.0 においての StringBuilder は「String.AppendInplace へのアクセスヘルパであり、capacity overflow に対する failsafe を提供するクラス」で、String は imuttable でなく repeatly editableだったんですよ。(ΩΩΩΩ< な、なんだってー)

StringBuilder の m_StringValue が無くなった事で動かなくなったコードが(アワワワワ

ちなみに超パフォーマンススペシフィックなシチュエーションに対してよく訓練された .NET er は

public static class StringBuilderExtention
{
    /// <summary>
    ///  ToString と同じ動作だけど、ここでコピーが発生するって事を示す為にワザトextentionする
    /// </summary>
    public static string CopyString( this StringBuilder sb ) { return sb.ToString(); }
}

とか、コピーを避ける為に m_StringValue を取り出して使うとかしてたりしてなかったり…

[T4 TIPS] T4テンプレートのコンパイルソースを得る方法


カスタムツールに

TextTemplatingFilePreprocessor

を指定すると .tt の処理を示す .cs が得られるよ。

HostSpecific を指定してないでいれば、System.Text / System.Globalizationぐらいにしか依存しないから、テキストの整形処理を得るには結構便利。

<%
using( var conn = new SqlConnection() )
using( var cmd = new SqlCommand( “select …”, conn )
{
    using( var reader = cmd.ExecuteReader() ) {
        while( reader.Read() ) {
%>
<%=reader.GetInt(0) %>,”<%=reader.GetString(1)%>”
<%
        }
    }
}
%>

とかやるともちろんの事ながらテキストにDBレコードをcsv化して出してくれるコードになるんで、csをプロジェクトに入れてビルドすれば csv が書ける。

 

改行ってどう書くんだっけとか。テキストの中に “ を書く方法ってどうやるんだっけとか…そんな事ではもう悩まない。

SQLサーバに登録されたストアドを展開する為の T4 テンプレート


表題の物のおすそ分けです。

SQLサーバに登録されているストアドを展開先DBに drop create で登録するSQLファイルを書き出してくれます。また再登録を行ったストアドの一覧と展開先での登録の突き合わせを行い、更新対象にならなかったストアドが有れば(前のバージョンでは存在したが現バージョンで存在しない物、展開元への登録忘れか要らなくなった物が残ってる)結果セットとして表示します。

使い方としては欲しいSQLのファイル名+拡張子 tt でプロジェクト内に保存して Visual Studio からカスタムツールの実行だけです。

こういう網羅性とか正確さが必要な事は絶対に手でやっては駄目で、必要な事を淡々とやってくれる機械に任せるのが一番だったりします。

 

<#@ template language="C#v3.5" debug="true" hostSpecific="true" inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation" #>
<#@ output extension=".sql" encoding="shift-jis" #>
<#@ assembly name="System.dll" #>
<#@ assembly name="System.Data.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Data" #>
<#@ import namespace="System.Data.SqlClient" #>
<#
string databaseName = "YOUR DATABASE NAME HERE!!";

string connectionString="Data Source=(local);Initial Catalog="+databaseName+";Integrated Security=true;";
string cmdText=@"
select ob.object_id, ob.name, mo.definition
    from sys.objects ob
        inner join sys.sql_modules mo on ob.object_id=mo.object_id
    where type=’P’ and name not like ‘sp_%’
    order by ob.name
";

using( SqlConnection conn = new SqlConnection(connectionString) )
using( SqlCommand cmd = new SqlCommand(cmdText,conn) )
{
    conn.Open();
    try
    {
#>
declare @procs table( name sysname not null)
declare @statement nvarchar(max)
<#
        using( SqlDataReader reader = cmd.ExecuteReader() )
        {
            while( reader.Read() )
            {
                int objectId = reader.GetInt32(0);
                string name=reader.GetString(1);
                string definition=reader.GetString(2);
#>
— <#=name#>
insert @procs (name) values(‘<#=name#>’)
if exists (select * from sys.objects where name='<#=name#>’ and type=’P’)
begin
    drop procedure <#=name#>
end
select @statement='<#=definition.Replace("’","”")#>’
exec sp_executesql @statement
<#
            }
        }
    }
    finally
    {
        conn.Close();
    }
}
#>
print ‘置き換えされなかったストアドプロシジャ(todo drop?)’
select name
    from sys.obects
    where type=’P’
        and name not like ‘sp_%’
        and name not in (select name from @procs)