ASP.NET MVCでヴァリデーションをどうしようか?→今回は自前で(´д`)

今週後半何をやっていたかというと、簡易的なヴァリデーションフレームワークを作っていました(・∀・)
現状、ASP.NET MVCでのヴァリデーションと言うと、Controllerでベタに書くか、IDataErrorInfoでやるか、DataAnnotationsを使った方法くらいが標準で。
他には、xValとかValidator Toolkit for ASP.NET MVCなんかがありますが。


でも、DataAnnotationsだと機能的にチト物足りないし、ヴァリデーションフレームワークってO/Rマッパーと同じように自由がきかない部分があって、色々と検討する必要があったりもして(・ω・)
っで、とりあえずJavaScriptでのクライアントヴァリデーションはやらないとして、ちょっとしたものであれば自分で作っても良いなと思い、簡易ヴァリデーションフレームワークを作ってみたたのでした。*1


っということで、以下、ヴァリデーションに関する雑記。

モデルとヴァリデーションの多重度

まず、ヴァリデーションって、そもそも何に対するヴァリデーションなのかという話がありますが。
入力(View/ViewModel)に対するヴァリデーションなのか、データ(Model)に対するヴァリデーションなのか(・ω・)


ASP.NET Web FormsのValidationコントロールなんかは、入力に対するヴァリデーションと言えるだろうし。
After Railsフレームワークなんかだと、ヴァリデーションはModel(ViewModel)に対して行い、ヴァリデーションとModelの関係は1対1であることを前提としているタイプのものも多いかと思います。
ASP.NET MVCのDefaultModelBinderで行われるIDataErrorInfoやDataAnnotationsによるヴァリデーションも、Model/ViewModelに対するヴァリデーションで、ヴァリデーションとModelの関係は1対1と言えるかと。


っで、必ずしもヴァリデーションとModelの関係は1対1じゃないと思うんだけど、どうよ(゚Д゚)?、っというのが本題。


例えば、Modelのコンテキスト(Modelに対する処理/Modelの状態)に応じて、ヴァリデーションルールが異なるようなアプリケーションの場合を考えて。
状態1の時はルールAをチェック、状態2の時はルールAとルールBをチェック、状態3の時はルールAとルールBとルールCのチェックをやりたいんだけど、フレームワークはそれをスマートに書けるような作りになっているのか(゚Д゚)?、っと。
処理/画面毎に別のViewModelを用意するとかいうやりかたもあるかもしれないけど(´д`;)


あるいはViewModelに対するヴァリデーションで、1つの画面に複数のボタンがある時に、押下されたボタンによってヴァリデーションの内容を変えたいんだけど、それには対応してる(゚Д゚)?、なんつって。
Web FormsのValidationコントロールでは、ValidationGroupがあるけどさ〜、って。


っということで、コンテキストに応じて1つのModelに対するヴァリデーションルールを変えたい場合には、ValidationGroupみたいグループ化する方法が1つあると思います(・∀・)
後は、ドメインモデルを採用するのではなく、Modelに対するロジックをServiceとかに分離してDTO+トランザクションスクリプトみたいにするように、ModelからValidationを分離しちゃうような方法もあるかと思ったりなんかして。*2

ヴァリデーションの呼び出しタイミング

いくつかのフレームワークでは、1Model-1ヴァリデーションを前提しているのと同じく、ヴァリデーションの呼び出しタイミングをControllerのメソッド呼び出し前に行うことを前提としているものもあったりします(・ω・)
DefaultModelBinderで行われる(以下略)。


っで、「入力検証」のヴァリデーションだけならそれでもたいして困らないわけですが。
ただ、ヴァリデーションっていうのは「入力検証」と「ビジネスルール」の検証があって(´ω`)


画面上に表示する検証結果の見せ方を考えた時に、「入力検証」のヴァリデーションをフレームワークに自動的に呼び出してもらうのではなく、「ビジネスルール」に対する検証処理との組み合わせを考えて、任意のタイミングでマニュアルで「入力検証」を行いたいなんていうニーズも無くはない、っと思ったりして。

PI(Persistence Ignorance)っというかVI(Validation Ignorance)みたいな

ヴァリデーションルールの指定方法としては、Modelのメンバに対してヴァリデーション用の属性を付加するタイプのフレームワークが多いかと思います(・ω・)


っで、っだ。
Model(not ViewModel)のメンバに付加する属性としては、ヴァリデーション用のものの他に、永続性処理用のものがあったりするわけですが。
永続性処理用の属性がついたModelを自動生成した場合、そこに手動でヴァリデーション用の属性をつけようとすると、そこでケンカすることになったりしてね(´・ω・`)
これが「処理」の追加だったら、partialで実装すればで良い事なんですが…。


っということで、この問題に対するにはどうするか(・ω・)?
Controllerでは永続処理用のModelは使用せず、ViewModelのみを使用するという手もありますが、それもちょっとエエエェェェ(´д`)な感じで。


ここは、ヴァリデーションルールを属性で指定する方法(主にViewModelで使用)の他に、ModelとValidationを分離して、別途Validationのみを定義できるような仕組みも欲しいと思います。
多重度の問題への対応にもなるし(・∀・)

その他、入出力フィルタ、自動補正

あと、ヴァリデーションに関係するその他の項目について。


ヴァリデーションと関連する項目として、入力値の自動補正や入出力フィルタの話なんかがあると思います(・ω・)
例えば、入力値として認めるのは全角カナだけなんだけど、半角カナだったらそれはエラーとせずに、全角変換して通しちゃうとかそういう話。
っで、これをやるには、フレームワークの入出力処理をフックできる拡張ポイントが必要になります。


っで、こういうことをやると、場合によっては値がバインドされたModelに対するヴァリデーションでは無くて、生のformの値に対するヴァリデーションを行いたいなんていうニーズもあったりなかったり。


まあ、今回は無いんだけどさ(・ω・)

私家版ヴァリデーションフレームワーク

っということで、以上を踏まえて。
Controllerでダラダラ検証コードを書けばどうにでもなるかもしれないけど、そんなのは嫌だし、

  • Modelとヴァリデーションの多重度を1対多にできる
  • ヴァリデーションの呼び出しタイミングをマニュアルにできる
  • ヴァリデーションの指定はアノテーション以外の方法でもできる
  • DataAnnotationsだと機能面でチト不満

っという要求も満たしたいと言うことで、今週は簡易的なヴァリデーションフレームワークを作っていたのでした(`・ω・´)


とりあえずこんなん。

っで、使い方として、一番単純なパターン(ViewModelに対して属性指定、ヴァリデーションの呼び出しはControllerの実行前)だとこんな感じにかけるようになっています(・ω・)

// ViewModel
public class AccountRegisterViewModel
{
    [ValidateRequired(Message="ユーザ名を入力してください。", Order=1)]
    [ValidateStringLength(Max=32, Message="ユーザ名が長すぎます。", Order=2)]
    public string UserName { get; set; }

    [ValidateRequired(Message="メールアドレスを入力してください。", Order=1)]
    [ValidateRegex(Expression=@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*", Message="メールアドレスの形式が正しくありません。", Order=2)]
    public string Email { get; set; }

    [ValidateRequired(Message="メールアドレス(確認)を入力してください。", Order=1)]
    [ValidateCompare(NameToCompare="Email", Message="メールアドレスが一致していません。", Order=2)]
    public string ConfirmEmail { get; set; }

    [ValidateMethod("_FORM", Message="メソッドによる複雑な検証に失敗しました。")]
    public bool CustomValidation()
    {
        // 複雑な検証をやりたかったらメソッドで
        return true;
    }
}
// Controller
public class AccountController : Controller
{
...
    // ValidatieModelAttributeはOnActionExecuting()で検証を実行します
    [AcceptVerbs(HttpVerbs.Post)]
    [ValidatieModel]
    public ActionResult Register(AccountRegisterViewModel model)
    {
        if( !ModelState.IsValid )
        {
            ...
        }

        ...
    }
}

これだけだと、他のヴァリデーションフレームワークとそう変わりませんが(´д`;)


以下、この簡易ヴァリデーションフレームワークの機能概要を少し書いてみます。

マニュアル/自動ヴァリデーション

ControllerのメソッドにValidatieModelAttribute( : FilterAttribute, IActionFilter)を指定すると、そのOnActionExecuting()でヴァリデーション処理を自動で呼び出してくれる仕組みで。
呼び出しタイミングを自分で制御したい場合、Validator.Validate()メソッドもしくはControllerExtensionで定義したValidate()拡張メソッドでヴァリデーションの実行が可能になっています。


以上により、ヴァリデーション呼び出しのタイミングの問題については対処(・∀・)


ちなみに、アクションメソッドの引数が1つの時は自動的にその引数が検証対象にしますが、引数が複数ある時はTargetプロパティで検証対象を指定する形。
また、検証対象のオブジェクトメンバにValidateAttributeが付加されている場合はそのルールでヴァリデーションを行うけれど、ValidationSet(後述)プロパティの指定により、別途ヴァリデーションルールを指定することも可能です。
あと、Groupプロパティで、ValidateAttributeのどのGroup(後述)でヴァリデーションを行うかの指定も可能です。

ヴァリデーションの種類

用意したヴァリデーションは以下の通り。

  • Required 必須チェック
  • Regex 正規表現チェック
  • StringLength 文字列長チェック
  • Compare 比較チェック(型チェック及び他メンバとの比較)
  • Range 範囲チェック
  • ArrayCount 項目数チェック
  • Method メソッドによるチェック

各ヴァリデーションクラスと、そのヴァリデーションを指定するためのValidateAttribute(派生クラス)が存在。*3
処理内容に関して、Required、Regex、StringLengthとかはまあ良いとして。


Compareは、Web FormsのCompareValidatorを参考にしたもの。
CompareType(Auto、String、Integer、Double、Date)、CompareOperator(Equal、NotEqual、GreaterThan、GreaterThanEqual、LessThan、LessThanEqual、DataTypeCheck)、NameToCompare、ValueToCompareとかのプロパティを持ち、モデルメンバ単体の型チェックや、他のメンバとの値比較が可能。


CompareValidatorに比べてサボっているのは、Currencyのチェックなんて使わねーよという所と、CompareType.Dateの時の扱いをちゃんと考えると面倒そうだったので、DateTime.Parse()もしくはFormatプロパティを追加してDateTime.ParseExact()で処理させているあたりかしら(´д`;)


後のArrayCountは配列数をチェックするもので、Methodヴァリデーションは次を参照。

メソッドによるヴァリデーション

モデルクラスにValidateMethodAttributeを付加した戻り値boolなメソッドを用意すると、そのメソッドを呼び出した結果も反映(`・ω・´)
単純な入力検証では済まず、複数メンバの値を使って検証を行いたいときの為に用意。


使用例は、AccountRegisterViewModelの記述から抜粋してこげな感じで。

// ViewModel
public class AccountRegisterViewModel
{
...
    [ValidateMethod("_FORM", Message="メソッドによる複雑な検証に失敗しました。")]
    public bool CustomValidation()
    {
        // 複雑な検証をやりたかったらメソッドで
        return true;
    }
}

ヴァリデーションのグループ化。

モデルとヴァリデーションの多重度の問題を解決する仕組みその1(・∀・)
ASP.NET Web FormsのValidationControlにおけるValidationGroupを参考。


以下、コードのイメージで説明。

public class HogeModel
{
    [ValidateRequired(Message="Aは必須です。", Group="Foo, Hoge")]
    public string A { get; set; }

    [ValidateRequired(Message="Bは必須です。", Group="Bar, Hoge")]
    public string B { get; set; }
}
var model = new HogeModel();
Validator.Validate( model, "Foo", errors ); Aのみエラー
Validator.Validate( model, "Bar", errors ); Bのみエラー
Validator.Validate( model, "Hoge", errors ); A、Bがエラー

モデルからヴァリデーション定義の分離

モデルとヴァリデーションの多重度の問題を解決する仕組みその2&VIの為の仕組み(・∀・)
abstract ValidationSetなクラスを作成して、ValidatieModelAttributeやValidator.Validate()メソッドにその型を指定することにより、そのルールでのヴァリデーションを実行可能。


こんな感じで。

class HogeValidationSet : ValidationSet
{
    protected override Validation[] GetValidations()
    {
        return new Validation[]
        {
            new RequiredValidation { Name = "UserName", Message = "ユーザ名を入力してください。" },
            new RequiredValidation { Name = "Email", Message = "メールアドレスを入力してください。" },
            new RegexValidation    { Name = "Email", Expression=@"...",  Message = "メールアドレスの形式が正しくありません。" },
...
        };
    }
}

フレームワークの内部処理について言えば、モデルのメンバに付加したValidateAttributeも、それからValidationSetを作成するために使用するものであって。
このフレームワークで管理するヴァリデーションルールとは、つまりValidationSetクラスで表されるもの。

ASP.NET MVCからの独立

一応、Mvc以下のクラス以外はASP.NET MVCに依存しないように作成(・ω・)
ITargetProvider(検証対象の抽象化)やIErrorProvider(検証結果の抽象化)もそのためのもの。



つーこって、ヴァリデーションの方針も決めたし、必要な下ごしらえは今週までで完了した感じ(・∀・)
来週からは、ボクちんも機能をガシガシ作るよ(`・ω・´)
とりあえずデザインがあがって来る前にVer. 1の実装を終えるのダ。

*1:昨日のプロパティキャッシュもその過程で生まれたものだったりして(・∀・)

*2:っというか、こういうニーズが発生する状況って、モデル設計の粒度が不味いって事かしらん(・ω・)?

*3:あと、コンクリエートなValidationSetを作るときに使うValidation派生クラスも同じ数だけ存在するんだけど…(´д`;)