kazuk は null に触れてしまった

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

月別アーカイブ: 5月 2010

Custom TypeDescriptionProvider で1レイヤ下から攻略する


System.ComponentModel.DataAnnotations 便利ですよね、アノテーションしっかりしてやって入力検証とUI構築をゴニョれば ASP.NET Dynamic Dataとか、同様にASP.NET MVC のモデル検証系なんかの面倒も見ているってわけで、これからの入力検証やその他 Naked Objects への方向性へ向かう基盤ライブラリって感じですかね。(必ずしもNaked Objectsは良いとは言い切れないんだけど、Naked Object にも良い点がイッパイあるんで良いとこどりができるのであれば取り組みたい素材ですな。)

ただねー、問題もいくつか無かったりもする。

  1. 顧客用語って問題。「品番」「型番」「商品コード」、実はこれら顧客用語でシステム内部的には string ItemIdってケース。Display属性は複数指定できないし、複数指定できたとしても今度はドレをだせば良いのさ!って問題に。(これは検証エラーのエラーメッセージとかにも影響を与える)
  2. 顧客固有コード空間に対する検証。顧客用語とおんなじ例だけど、ある顧客は ItemIdのフォーマットとして [A-Z][A-Z0-9]*(-[A-Z0-9]+)? しか許さない、長さは18文字までの可変長。とある顧客は 16文字の固定長で大文字英数字、8桁目と12桁目にハイフンが必須。「おぉっと、属性で検証条件をコードにマークアップって良いのか?」

1の方にはリソース化とかを介してメッセージフォーマッターでなんとかすれば良いって意味で逃げ道があるんだけど、2には根本的には逃げ道無かったりする様に見える。

この2に対する逃げ道のお話。

 

ここで .NET Framework の(ASP.NETやWinFormsその他)各種コントロールのデータバインディング機構や DataAnnotations がどっからどのように型情報を取ってるかの順序の超おおざっぱな話が以下の通り(最底辺はちょっとあやふや)

  1. System.Runtime.Remoting.IRemotingTypeInfo とかシステムの外界、リモートオブジェクトでない場合にはアセンブリのメタデータ
  2. System.Reflection から得られる System.Type。1で得られた型情報を System.Type 型に見せているのがここ
  3. System.ComponentModel.TypeDescriptorから得られる MemberDescriptor、 TypeDescriptor.GetProperties とかで取れる PropertyDescriptorやEventDescriptor

3に対するお客は色々多岐に分かれるんだけど、主にアプリケーション層に属する人たちや、Visual Studioでのデザイン時動作等、本当にめっぽう多岐にわたる。

たとえば、 ASP.NET DynamicForms なんかも3の層を介して型情報を貰ってるわけ。証拠はこんな感じ。

TableProvider.GetTypeDescriptor メソッド (System.Web.DynamicData.ModelProviders)

要するに、この TypeDescriptorを Custom TypeDescriptionProvider を使ってカスタムすると、DataAnnotations でのモデル検証に関する属性類をランタイムに!動的に!まるっと!全部!置き換えできるって寸法。

こいつらカスタムする時はみんな派生クラスを作らないといけないんでちっと面倒なんだけど、派生クラスを作る必要がある物は以下の物。

  • TypeDescriptionProvider
    • 基底コンストラクタが2通りある、誰かに親になってもらうとbaseがそれなりに動くようになるので誰かに親になってもらうのが吉、.NET FrameworkのデフォルトのTypeDescriptionProviderは隠されて実装されているので親のなり手を探すのに困る。自分のやってる方法としてはTypeDescriptor.GetProvider(typeof (object))とすれば少なくとも object に反応してるTypeDescriptionProviderが取れるんで、親になって頂く(objectに対するTypeDescriptorを変えてるとか超絶アブストラクションしてる所ではうまくないかもしれないけど、そんな変態環境で動かないのは「仕様です」)
    • GetTypeDescriptorをoverrideする。って言っても parent になってくれている TypeDescriptionProvider にbaseに一回ICustomTypeDescriptorを作ってもらってからそれを舐めながら変換するので↓で用意する CustomTypeDescriptor の派生を new する時に base.GetTypeDescriptor で帰ってきた物を渡すだけ
  • CustomTypeDescriptor
    • 本来的には ICustomTypeDescriptor を実装するんだけど、こいつを派生すれば余計な事をしないで済むので楽
    • GetProperties をオーバライドする(のが今回の目的)、PropertyDescriptorCollection を base.GetProperties した結果を元にいじる。だけど PropertyDescriptorは根本 immutable なんで出来た物はいじれない。複製しながらいじるんだけど、PropertyDescriptor のコンストラクタは protectedなんで派生しないと駄目。複製する時にコンストラクタに渡す Attributes[] が今回入れ替えようとしている属性セットなんで、これをドッカからホゲって取ってくる様にする。
  • PropertyDescriptor
    • PropertyDescriptor のコンストラクタがprotectedだから(ケチ!)派生してるだけ、abstractなメソッドに対してはコンストラクタで貰える MemberDescriptor/PropertyDescriptorのインスタンスを呼べばよろしいわけ。実装してれば気付くと思うけど、ここの GetValue とか SetValue をどこか遠くにつれていくと各種コントロールからデータバインディングを経由して送り込まれるデータをよろしくできる。

んで、これらの派生クラスを作ったうえである型についてCustom TypeDescriptorによる属性セットの置き換えをしたければ TypeDescriptionProviderAttribute でその型をマークアップするか、TypeDescriptor.AddProvider で実行時にプロバイダを突っ込む。

 

本来的には存在しない物を PropertyDescriptor を作ってさえあげればプロパティがある様に見せかける事とか、親なんて無視でフルカスタマイズすれば全く違う構造にプロパティセットやイベントセットがある物として見せる事だって可能。やりすぎるとプログラムの構造と実行時の構造が乖離してわけわかめになるんで自殺行為なんだが DataAnnotationsでの検証がコードで固定されるのがネックならば手を出す価値ありですぞ。

 

こうして Custom したTypeDescriptorが入れ替え用の属性セットを取ってくる所が Custom VirtualPathProvider で乗っ取られてるとか乗っ取られてないとか…

「フレームワーク」と書いて「床下の魑魅魍魎」と読むのがよさげな実装を繰り返す奴にはご注意を

広告

事例記事がMSサイトに上がってます。


 

http://www.microsoft.com/web/page.aspx/ja-jp/inspiration/crosswarp

って事で、事例記事が大きく上がってますね。

ショーケースのなか、うちだけって事で、ははははは、一番のりのメリットというか、すごい占有率。

ちなみに 独自CMS の中身は随所随所にキャッシュを配置している等の工夫はありますが System.Web.Routing からルートされてきたリクエストに応じて、テンプレートをパース→コード生成→コンパイル→実行っていう、定番的なWebアプリケーション実行エンジンとなってます。

テンプレートの記述言語は

  1. LINQ to SQL の結果セットをレンダリングする

って事をドメインとして、それに必要なシンタックスだけの超簡単な物だったりします。(現状はLINQ to SQLだけど、LINQ式という構文が一致すれば良いはずなんでクエリプロセッサは乗換可能なはず)

これを C# で IHttpHandlerとして書くとして、「結果セットはできるだけキャッシュする」ならコウスルをコードジェネレータとして仕込んでるだけ。

後はトータルとしてエンジンをドライブしてあげれば LINQ to SQL の結果が IHttpHandler でどかっとレンダリングされるって形ですね。

最終的な実行ステージでは IHttpHandler がキャッシュ制御しながらLINQ to SQLをドライブしているだけなんでキャッシュのヒットレートが十分であれば単純な Response.Write の連続コールループに遜色の無いアウトプットスループットが出せます。まぁ、さらにIIS7のカーネルモードキャッシュをイネーブルするのがデフォルトコンフィギュレーションって事で「目指せ!キャッシュモンスター」な実装でございます。

x64でこんなキャッシュモンスターを動かすと当然の事に大量にメモリを消費します…。元気に走ってるとこんなもんです。

image

w3wp.exeにメモリ2GBをもってかれているの図ですね。あぁ、安心して下さい。このWebサーバ、メモリ16GB乗ってます。設計上こんぐらいまで使うのは当然なんです。

 

この凶悪なテンプレートエンジンを ASP.NET MVC の ViewEngineとして再利用するとかはこの人のなせる技。

てな訳で、事例記事よりちょっと技術よりな身の無い解説でございました。

[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 が書ける。

 

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