Asp.Net MVC3: Set custom IServiceProvider in ValidationContext so validators can resolve services

后端 未结 3 1072
被撕碎了的回忆
被撕碎了的回忆 2020-11-30 06:42

Update 18th December 2012

Since this question seems to be getting quite a few views, I should point out that the accepted answer is not

3条回答
  •  余生分开走
    2020-11-30 07:22

    On MVC 5.2, you can leveragesteal @Andras's answer and the MVC source and:

    1. Derive a DataAnnotationsModelValidatorEx from DataAnnotationsModelValidator

    namespace System.Web.Mvc
    {
        // From https://aspnetwebstack.codeplex.com/SourceControl/latest#src/System.Web.Mvc/DataAnnotationsModelValidator.cs
        // commit 5fa60ca38b58, Apr 02, 2015
        // Only diff is adding of secton guarded by THERE_IS_A_BETTER_EXTENSION_POINT
        public class DataAnnotationsModelValidatorEx : DataAnnotationsModelValidator
        {
            readonly bool _shouldHotwireValidationContextServiceProviderToDependencyResolver;
    
            public DataAnnotationsModelValidatorEx(
                ModelMetadata metadata, ControllerContext context, ValidationAttribute attribute,
                bool shouldHotwireValidationContextServiceProviderToDependencyResolver=false)
                : base(metadata, context, attribute)
            {
               _shouldHotwireValidationContextServiceProviderToDependencyResolver =
                    shouldHotwireValidationContextServiceProviderToDependencyResolver;
            }
        }
    }
    

    2. Clone the base impl of public override IEnumerable Validate(object container)

    3. Apply the hack Render the elegant incision after Validate creates the context:-

    public override IEnumerable 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). string memberName = Metadata.PropertyName ?? Metadata.ModelType.Name; ValidationContext context = new ValidationContext(container ?? Metadata.Model) { DisplayName = Metadata.GetDisplayName(), MemberName = memberName };

    #if !THERE_IS_A_BETTER_EXTENSION_POINT
       if(_shouldHotwireValidationContextServiceProviderToDependencyResolver 
           && Attribute.RequiresValidationContext)
           context.InitializeServiceProvider(DependencyResolver.Current.GetService);
    #endif
    
       ValidationResult result = Attribute.GetValidationResult(Metadata.Model, context);
        if (result != ValidationResult.Success)
        {
            // ModelValidationResult.MemberName is used by invoking validators (such as ModelValidator) to
            // construct the ModelKey for ModelStateDictionary. When validating at type level we want to append the
            // returned MemberNames if specified (e.g. person.Address.FirstName). For property validation, the
            // ModelKey can be constructed using the ModelMetadata and we should ignore MemberName (we don't want
            // (person.Name.Name). However the invoking validator does not have a way to distinguish between these two
            // cases. Consequently we'll only set MemberName if this validation returns a MemberName that is different
            // from the property being validated.
    
           string errorMemberName = result.MemberNames.FirstOrDefault();
            if (String.Equals(errorMemberName, memberName, StringComparison.Ordinal))
            {
                errorMemberName = null;
            }
    
           var validationResult = new ModelValidationResult
            {
                Message = result.ErrorMessage,
                MemberName = errorMemberName
            };
    
           return new ModelValidationResult[] { validationResult };
        }
    
       return Enumerable.Empty();
    }
    

    4. Tell MVC about the new DataAnnotationsModelValidatorProvider in town

    after your Global.asax does DependencyResolver.SetResolver(new AutofacDependencyResolver(container)) :-

    DataAnnotationsModelValidatorProvider.RegisterAdapterFactory(
        typeof(ValidatorServiceAttribute),
        (metadata, context, attribute) => new DataAnnotationsModelValidatorEx(metadata, context, attribute, true));
    

    5. Use your imagination to abuse your new Service Locator consume using ctor injection via GetService in your ValidationAttribute, for example:

    public class ValidatorServiceAttribute : ValidationAttribute
    {
        readonly Type _serviceType;
    
        public ValidatorServiceAttribute(Type serviceType)
        {
            _serviceType = serviceType;
        }
    
        protected override ValidationResult IsValid(
            object value, 
            ValidationContext validationContext)
        {
            var validator = CreateValidatorService(validationContext);
            var instance = validationContext.ObjectInstance;
            var resultOrValidationResultEmpty = validator.Validate(instance, value);
            if (resultOrValidationResultEmpty == ValidationResult.Success)
                return resultOrValidationResultEmpty;
            if (resultOrValidationResultEmpty.ErrorMessage == string.Empty)
                return new ValidationResult(ErrorMessage);
            return resultOrValidationResultEmpty;
        }
    
        IModelValidator CreateValidatorService(ValidationContext validationContext)
        {
            return (IModelValidator)validationContext.GetService(_serviceType);
        }
    }
    

    Allows you to slap it on your model:-

    class MyModel 
    {
        ...
        [Required, StringLength(42)]
        [ValidatorService(typeof(MyDiDependentValidator), 
            ErrorMessage = "It's simply unacceptable")]
        public string MyProperty { get; set; }
        ....
    }
    

    which wires it to a:

    public class MyDiDependentValidator : Validator
    {
        readonly IUnitOfWork _iLoveWrappingStuff;
    
        public MyDiDependentValidator(IUnitOfWork iLoveWrappingStuff)
        {
            _iLoveWrappingStuff = iLoveWrappingStuff;
        }
    
        protected override bool IsValid(MyModel instance, object value)
        {
            var attempted = (string)value;
            return _iLoveWrappingStuff.SaysCanHazCheez(instance, attempted);
        }
    }
    

    The preceding two are connected by:

    interface IModelValidator
    {
        ValidationResult Validate(object instance, object value);
    }
    
    public abstract class Validator : IModelValidator
    {
        protected virtual bool IsValid(T instance, object value)
        {
            throw new NotImplementedException(
                "TODO: implement bool IsValid(T instance, object value)" +
                " or ValidationResult Validate(T instance, object value)");
        }
    
        protected virtual ValidationResult Validate(T instance, object value)
        {
            return IsValid(instance, value) 
                ? ValidationResult.Success 
                : new ValidationResult("");
        }
    
        ValidationResult IModelValidator.Validate(object instance, object value)
        {
            return Validate((T)instance, value);
        }
    }
    

    I'm open to corrections, but most of all, ASP.NET team, would you be open to a PR to add a constructor with this facility to DataAnnotationsModelValidator?

提交回复
热议问题