ASPに深く入り込む.NET MVCの10:サーバ側Model Validation

27757 ワード

ASP.NET MVC 3は、サービス側とクライアント・スクリプト検証の2つの大きなタイプの検証をサポートします.まず、サービス・エンドの検証について説明します.前述したように、サーバ側の検証はモデルバインディング時に行われ、DefaultModelBinderでは次のような方法で検証がトリガーされます.
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である場合、対応する検証メソッドも呼び出されます.