miso_soup3 Blog

主に ASP.NET 関連について書いています。

ValidationAttribute の検証メソッド内で、外部コンポーネントを利用する

ASP.NET MVC の、カスタム検証属性クラス内で、
IRepository 等の外部コンポーネントを利用する方法です。

これを上手く使えば、データベースを使った検証でも、
Controller 以降に書いたりせずとも、モデルバインダ対象のプロパティに属性をつけるだけで
実現できたりします。

やりたいこと
[MyName] // ←カスタム検証属性で・・・
public String Name { get; set; }
//↓こんな風に、IRepository を使いたい
public override bool IsValid(object value)
{
 var repository = ?? ;
 //..なんとかして Repository を取得して検証したい。
   // new Repository() はしたくない。(またはできない。) 
}
概要

実装方法には、
.NET Framework の DataAnnotations の話と
ASP.NET MVC の拡張の話が混ざります。

全体像はこんなかんじです。

DataAnnotations 側の実装

カスタム検証属性は、ValidationAttricute クラスを継承するのが一般的ですが、
継承できるメソッドは、
bool IsValid(object value); の他にも、

ValidationResult IsValid(object value, ValidationContext validationContext);

というメソッドもあります。
このメソッドの引数 ValidationContext には、

object GetService(Type serviceType);

という指定した型のサービスを取得できるメソッドがあります。

つまり、

ValidationAttribute を継承したカスタム検証属性内で
↓こんな風に IsValid メソッドを継承させて、

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
   // ValidationContext から IPersonNameValidator という外部コンポーネントを取得
 var validator = validationContext.GetService(typeof(IPersonNameValidator)) as IPersonNameValidator;
 // validator で検証...
}

という風にかけば、外部コンポーネントを利用して検証を実行できるというわけです。

じゃあ、この外部コンポーネントをどうやって差し込むのか、
ValidationContext はどこからきているのか、という話になるのですが、
この話は検証属性から外れて、DataAnnotation を使った検証ロジック全体の話に移ります。

DataAnnotation を使って検証する場合は、(属性を利用して検証する場合、)
検証対象はもちろん、 ValidationContext のインスタンスを用意しないといけません。

検証例

//ValidationContext のインスタンス取得
ValidationContext context = new ValidationContext(instance: 検証対象, serviceProvider: null, items: null);
 //例:ある検証属性で検証
 ValidationResult result = Attribute.GetValidationResult(検証対象, context);
 //例:対象全体を検証
 Validator.TryValidateObject(model, context, results, validateAllProperties: true);

その必要なインスタンス、ValidationContext のコンストラクタはこの様になっています。

ValidationContext(object instance, IServiceProvider serviceProvider, IDictionary<object, object> items);

ValidationContext のコンストラクタの引数に、IServiceProvider があるので、
ここに外部コンポーネントを提供してくれる IServiceProvider を引数にいれてあげれば、
さっきの検証属性の IsValid メソッドで、ValidationContext.GetService(...) から外部コンポーネントを取得することができます。

.NET Framework の DataAnnnotation の話はここまでです。これだけでは実現できません。
自分で実装した IServiceProvider が入っている ValidationContext を使って、検証を行なってくれるように、
ASP.NET MVC フレームワークに登録する必要があります。

ASP.NET MVC フレームワークに登録する方法

なにを登録するのか

ASP.NET MVC には、”DataAnnotations の検証属性を使ってモデルを検証する” という役割の検証クラスがあります。
また、そのクラスを提供してくれる、提供クラスもあります。

前者は、”DataAnnotationsModelValidator”、
後者は、”DataAnnotationsModelValidatorProvider” です。

肝心の ValidationContext のインスタンスは、検証クラスである DataAnnotationsModelValidator の中で new() されているので、
開発者が手を加えるスキはありません。(IServiceProvider を仕込むスキはありません。)

なので、DataAnnotationsModelValidator に変わる Validator クラス を自分で作成して、
提供クラスである DataAnnotationsModelValidatorProvider が、自分で作成した Validator クラス を提供させるようにします。

DataAnnotationsModelValidatorProvider に、自作 Validator クラスを登録する。
これが、今回のASP.NET MVC フレームワークに登録する実装内容になります。

カスタム DataAnnotationsModelValidator

DataAnnotationsModelValidator に変わる Validator クラスのコードは、下のGitHub上にあります。
CustomDataAnnotationsModelValidator.cs

中身(IServiceProvider をいれているところ)

public override IEnumerable<ModelValidationResult> Validate(object container)
{
 //IServiceProviderを設定する。
 ValidationContext context = new ValidationContext(container ?? Metadata.Model, serviceProvider, null);
   ....

コンストラクタの引数に、IServiceProvider を定義し、DataAnnotationsModelValidator を継承します。
ValidationContext のインスタンスを作成する部分以外は、継承元のクラスと同じです。

DataAnnotationsModelValidatorProvider に登録

IServiceProvider を実装したクラスを用意し、
作成した Validator クラスを、Provider に登録します。

IServiceProvider は、StructureMap や Unity を使って実装するのが簡単ですが、
手作りでも構いません。

場所は Glocal.asax.cx などに記述します。

//IoCを使って、IServiceProvider を実装
IServiceProvider serivceProvider = new StructureMapServiceProvider();

//Provider に登録
DataAnnotationsModelValidatorProvider.RegisterDefaultAdapterFactory(
  (metadata, context, attribute) =>
  new CustomDataAnnotationsModelValidator(serivceProvider, metadata, context, attribute)
				       // ↑ServiceProvider いれる。
);

おしまい

実装方法は以上になります。
これで、検証属性の中で、IRepository などの外部コンポーネントを利用できるようになります。

サンプルを GitHub にあげました。
CustomModelValidatorTry

実用度については・・・・
アプリケーション内でよく頻発する検証で、
検証が複雑(WebServiceを利用するとか)な場合に使えるかもしれません。

私は使ったことがないです。。。