ASPに深く入り込む.NET MVCの10:サーバ側Model Validation
27757 ワード
ASP.NET MVC 3は、サービス側とクライアント・スクリプト検証の2つの大きなタイプの検証をサポートします.まず、サービス・エンドの検証について説明します.前述したように、サーバ側の検証はモデルバインディング時に行われ、DefaultModelBinderでは次のような方法で検証がトリガーされます.
ここで、OnModelUpdatedメソッドは、認証ロジックを実際にトリガーする場所です.
まず、ModelValidatorを取得します.このModelValidatorはCompositeModelValidatorで、Validateメソッドを呼び出します.
まずmodelのproperty上のattributeを検索し、それをIModelValidatorオブジェクトに変換します.ここではアダプタモードを使用しています.GetValidatorsメソッドを見てください.これはModelMetaDataのメソッドです.
ModelValidatorProvidersは次のように定義されています.
最も多く使われているのはDataAnnotationsModelValidatorProviderです.彼はAssociatedValidatorProviderから継承されています.GetValidatorsメソッドを見てください.
まず、現在のmetadataがmodelの1つの属性であるか、それともmodelオブジェクト自体がmodel validatorを得るために2つの異なる方法をそれぞれ呼び出しているかを判断する.この2つの方法の違いは、反射によってタイプと属性(Proerty)に配置された属性(Attribute)をそれぞれ取得し、次に抽象方法GetValidatorsを呼び出し、この抽象方法はDataAnnotationsModelValidatorProviderにおいて、以下のように実現される.
まずnullのフィールドにrequiredプロパティをデフォルトで追加します.次に、すべてのラベルのタイプがValidationAttributeの属性を巡回します.各属性について、DataAnnotationsModelValidationFactoryを見つける必要があります.このfactoryは委任です.
ここではAdapterモードを用いて,属性の検証方法によりModelValidatorに変換したと考えられる.MVCは各種の異なる検証タイプに対していくつかの対応するadapter factory(削除あり)を提供した.
これらの対応するadapter factoryは主にクライアント検証のサポートを提供するため、以下ではクライアント検証について説明します.残りの機能はDefaultAttributeFactoryと似ています.
DataAnnotaionsModelValidatorの主な方法は次のとおりです.
実際、RangeAttributeAdapterのようなクラスはDataAnnotationsModelValidatorから継承されており、サービス側の検証ではValidateメソッドは同じです.検証の結果はValidationResultオブジェクトとして統一的に表示され、bindingContextに配置される.ModelStateにあります.ページをレンダリングすると、ModelStateからエラーメッセージが取り出され、ページに表示されます.以上の解析から、サービス側検証のプロセスは、modelに対して、まず各属性のvalidation attributeを取得し、その検証方法を呼び出し、この属性がIValidatableObjectタイプであれば、同様にその検証方法を呼び出すことが分かる.オブジェクト自体にvalidationattributeがある場合、またはそれ自体がIValidateableObjectである場合、対応する検証メソッドも呼び出されます.
internal void BindComplexElementalModel(ControllerContext controllerContext, ModelBindingContext bindingContext, object model) {
// need to replace the property filter + model object and create an inner binding context
ModelBindingContext newBindingContext = CreateComplexElementalModelBindingContext(controllerContext, bindingContext, model);
// validation
if (OnModelUpdating(controllerContext, newBindingContext)) {
BindProperties(controllerContext, newBindingContext);
OnModelUpdated(controllerContext, newBindingContext);
}
ここで、OnModelUpdatedメソッドは、認証ロジックを実際にトリガーする場所です.
protected virtual void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext) {
Dictionary<string, bool> startedValid = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
var res = ModelValidator.GetModelValidator(bindingContext.ModelMetadata, controllerContext).Validate(null);
foreach (ModelValidationResult validationResult in res) {
string subPropertyName = CreateSubPropertyName(bindingContext.ModelName, validationResult.MemberName);
if (!startedValid.ContainsKey(subPropertyName)) {
startedValid[subPropertyName] = bindingContext.ModelState.IsValidField(subPropertyName);
}
if (startedValid[subPropertyName]) {
bindingContext.ModelState.AddModelError(subPropertyName, validationResult.Message);
}
}
}
まず、ModelValidatorを取得します.このModelValidatorはCompositeModelValidatorで、Validateメソッドを呼び出します.
public override IEnumerable<ModelValidationResult> Validate(object container) {
bool propertiesValid = true;
foreach (ModelMetadata propertyMetadata in Metadata.Properties) {
foreach (ModelValidator propertyValidator in propertyMetadata.GetValidators(ControllerContext)) {
foreach (ModelValidationResult propertyResult in propertyValidator.Validate(Metadata.Model)) {
propertiesValid = false;
yield return new ModelValidationResult {
MemberName = DefaultModelBinder.CreateSubPropertyName(propertyMetadata.PropertyName, propertyResult.MemberName),
Message = propertyResult.Message
};
}
}
}
if (propertiesValid) {
var typeValidators = Metadata.GetValidators(ControllerContext);
foreach (ModelValidator typeValidator in typeValidators)
{
foreach (ModelValidationResult typeResult in typeValidator.Validate(container)) {
yield return typeResult;
}
}
}
}
まずmodelのproperty上のattributeを検索し、それをIModelValidatorオブジェクトに変換します.ここではアダプタモードを使用しています.GetValidatorsメソッドを見てください.これはModelMetaDataのメソッドです.
public virtual IEnumerable<ModelValidator> GetValidators(ControllerContext context) {
return ModelValidatorProviders.Providers.GetValidators(this, context);
}
ModelValidatorProvidersは次のように定義されています.
public static class ModelValidatorProviders {
private static readonly ModelValidatorProviderCollection _providers = new ModelValidatorProviderCollection() {
new DataAnnotationsModelValidatorProvider(),
new DataErrorInfoModelValidatorProvider(),
new ClientDataTypeModelValidatorProvider()
};
public static ModelValidatorProviderCollection Providers {
get {
return _providers;
}
}
}
最も多く使われているのはDataAnnotationsModelValidatorProviderです.彼はAssociatedValidatorProviderから継承されています.GetValidatorsメソッドを見てください.
public override sealed IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context) {
if (metadata.ContainerType != null && !String.IsNullOrEmpty(metadata.PropertyName)) {
return GetValidatorsForProperty(metadata, context);
}
return GetValidatorsForType(metadata, context);
}
まず、現在のmetadataがmodelの1つの属性であるか、それともmodelオブジェクト自体がmodel validatorを得るために2つの異なる方法をそれぞれ呼び出しているかを判断する.この2つの方法の違いは、反射によってタイプと属性(Proerty)に配置された属性(Attribute)をそれぞれ取得し、次に抽象方法GetValidatorsを呼び出し、この抽象方法はDataAnnotationsModelValidatorProviderにおいて、以下のように実現される.
protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes) {
_adaptersLock.EnterReadLock();
try {
List<ModelValidator> results = new List<ModelValidator>();
// Add an implied [Required] attribute for any non-nullable value type,
// unless they've configured us not to do that.
if (AddImplicitRequiredAttributeForValueTypes &&
metadata.IsRequired &&
!attributes.Any(a => a is RequiredAttribute)) {
attributes = attributes.Concat(new[] { new RequiredAttribute() });
}
// Produce a validator for each validation attribute we find
foreach (ValidationAttribute attribute in attributes.OfType<ValidationAttribute>()) {
DataAnnotationsModelValidationFactory factory;
if (!AttributeFactories.TryGetValue(attribute.GetType(), out factory)) {
factory = DefaultAttributeFactory;
}
results.Add(factory(metadata, context, attribute));
}
// Produce a validator if the type supports IValidatableObject
if (typeof(IValidatableObject).IsAssignableFrom(metadata.ModelType)) {
DataAnnotationsValidatableObjectAdapterFactory factory;
if (!ValidatableFactories.TryGetValue(metadata.ModelType, out factory)) {
factory = DefaultValidatableFactory;
}
results.Add(factory(metadata, context));
}
return results;
}
finally {
_adaptersLock.ExitReadLock();
}
}
まずnullのフィールドにrequiredプロパティをデフォルトで追加します.次に、すべてのラベルのタイプがValidationAttributeの属性を巡回します.各属性について、DataAnnotationsModelValidationFactoryを見つける必要があります.このfactoryは委任です.
public delegate ModelValidator DataAnnotationsModelValidationFactory(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute);
ここではAdapterモードを用いて,属性の検証方法によりModelValidatorに変換したと考えられる.MVCは各種の異なる検証タイプに対していくつかの対応するadapter factory(削除あり)を提供した.
internal static Dictionary<Type, DataAnnotationsModelValidationFactory> AttributeFactories = new Dictionary<Type, DataAnnotationsModelValidationFactory>() {
{
typeof(RangeAttribute),
(metadata, context, attribute) => new RangeAttributeAdapter(metadata, context, (RangeAttribute)attribute)
},
//…
};
これらの対応するadapter factoryは主にクライアント検証のサポートを提供するため、以下ではクライアント検証について説明します.残りの機能はDefaultAttributeFactoryと似ています.
internal static DataAnnotationsModelValidationFactory DefaultAttributeFactory =
(metadata, context, attribute) => new DataAnnotationsModelValidator(metadata, context, attribute);
DataAnnotaionsModelValidatorの主な方法は次のとおりです.
public class DataAnnotationsModelValidator : ModelValidator {
public DataAnnotationsModelValidator(ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute)
: base(metadata, context) {
if (attribute == null) {
throw new ArgumentNullException("attribute");
}
Attribute = attribute;
}
public override IEnumerable<ModelValidationResult> Validate(object container) {
// Per the WCF RIA Services team, instance can never be null (if you have
// no parent, you pass yourself for the "instance" parameter).
ValidationContext context = new ValidationContext(container ?? Metadata.Model, null, null);
context.DisplayName = Metadata.GetDisplayName();
ValidationResult result = Attribute.GetValidationResult(Metadata.Model, context);
if (result != ValidationResult.Success) {
yield return new ModelValidationResult {
Message = result.ErrorMessage
};
}
}
}
実際、RangeAttributeAdapterのようなクラスはDataAnnotationsModelValidatorから継承されており、サービス側の検証ではValidateメソッドは同じです.検証の結果はValidationResultオブジェクトとして統一的に表示され、bindingContextに配置される.ModelStateにあります.ページをレンダリングすると、ModelStateからエラーメッセージが取り出され、ページに表示されます.以上の解析から、サービス側検証のプロセスは、modelに対して、まず各属性のvalidation attributeを取得し、その検証方法を呼び出し、この属性がIValidatableObjectタイプであれば、同様にその検証方法を呼び出すことが分かる.オブジェクト自体にvalidationattributeがある場合、またはそれ自体がIValidateableObjectである場合、対応する検証メソッドも呼び出されます.