kazuk は null に触れてしまった

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

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!

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。