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

后端 未结 3 1058
被撕碎了的回忆
被撕碎了的回忆 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<ModelValidationResult> 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<ModelValidationResult>();
    }
    

    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<MyModel>
    {
        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<T> : 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?

    0 讨论(0)
  • 2020-11-30 07:27

    Have you thought about creating a model validator, using a modelValidatorProvider, instead of using validation attributes? This way you're not dependant on ValidationAttribute but can create your own validation implementation (this will work in addition the existing DataAnnotations validation).

    http://msdn.microsoft.com/en-us/library/system.web.mvc.modelvalidatorprovider.aspx

    http://dotnetslackers.com/articles/aspnet/Experience-ASP-NET-MVC-3-Beta-the-New-Dependency-Injection-Support-Part2.aspx#s10-new-support-for-validator-provider

    http://dotnetslackers.com/articles/aspnet/Customizing-ASP-NET-MVC-2-Metadata-and-Validation.aspx#s2-validation

    0 讨论(0)
  • 2020-11-30 07:27

    Update

    In addition to the class shown below, I've done a similar thing for IValidatableObject implementations as well (short notes towards the end of the answer instead of a full code sample because then the answer just gets too long) - I've added the code for that class as well in response to a comment - it does make the answer very long, but at least you'll have all the code you need.

    Original

    Since I'm targeting ValidationAttribute-based validation at the moment I researched where MVC creates the ValidationContext that gets fed to the GetValidationResult method of that class.

    Turns out it's in the DataAnnotationsModelValidator's Validate method:

    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
        };
      }
    }
    

    (Copied and reformatted from MVC3 RTM Source)

    So I figured some extensibility here would be in order:

    public class DataAnnotationsModelValidatorEx : DataAnnotationsModelValidator
    {
      public DataAnnotationsModelValidatorEx(
        ModelMetadata metadata, 
        ControllerContext context, 
        ValidationAttribute attribute)
        : base(metadata, context, attribute)
      {
      }
    
      public override IEnumerable<ModelValidationResult> Validate(object container)
      {
        ValidationContext context = CreateValidationContext(container);
    
        ValidationResult result = 
          Attribute.GetValidationResult(Metadata.Model, context);
    
        if (result != ValidationResult.Success)
        {
          yield return new ModelValidationResult
          {
            Message = result.ErrorMessage
          };
        }
      }
    
      // begin Extensibility
    
      protected virtual ValidationContext CreateValidationContext(object container)
      {
        IServiceProvider serviceProvider = CreateServiceProvider(container);
        //TODO: add virtual method perhaps for the third parameter?
        ValidationContext context = new ValidationContext(
          container ?? Metadata.Model, 
          serviceProvider, 
          null);
        context.DisplayName = Metadata.GetDisplayName();
        return context;
      }
    
      protected virtual IServiceProvider CreateServiceProvider(object container)
      {
        IServiceProvider serviceProvider = null;
    
        IDependant dependantController = 
          ControllerContext.Controller as IDependant;
    
        if (dependantController != null && dependantController.Resolver != null)
          serviceProvider = new ResolverServiceProviderWrapper
                            (dependantController.Resolver);
        else
          serviceProvider = ControllerContext.Controller as IServiceProvider;
        return serviceProvider;
      }
    }
    

    So I check first for my IDependant interface from the controller, in which case I create an instance of a wrapper class that acts as an adapter between my IDependencyResolver interface and System.IServiceProvider.

    I thought I'd also handle cases where a controller itself is an IServiceProvider too (not that that applies in my case - but it's a more general solution).

    Then I make the DataAnnotationsModelValidatorProvider use this validator by default, instead of the original:

    //register the new factory over the top of the standard one.
    DataAnnotationsModelValidatorProvider.RegisterDefaultAdapterFactory(
      (metadata, context, attribute) => 
        new DataAnnotationsModelValidatorEx(metadata, context, attribute));
    

    Now 'normal' ValidationAttribute-based validators, can resolve services:

    public class ExampleAttribute : ValidationAttribute
    {
      protected override ValidationResult 
        IsValid(object value, ValidationContext validationContext)
      {
        ICardTypeService service = 
          (ICardTypeService)validationContext.GetService(typeof(ICardTypeService));
      }
    }
    

    This still leaves direct ModelValidator-derived needing to be reimplemented to support the same technique - although they already have access to the ControllerContext, so it's less of an issue.

    Update

    A similar thing has to be done if you want IValidatableObject-implementing types to be able to resolve services during the implementation of Validate without having to keep deriving your own adapters for each type.

    • Derive a new class from ValidatableObjectAdapter, I called it ValidatableObjectAdapterEx
    • from MVCs v3 RTM source, copy the Validate and ConvertResults private method of that class.
    • Adjust the first method to remove references to internal MVC resources, and
    • change how the ValidationContext is constructed

    Update (in response to comment below)

    Here's the code for the ValidatableObjectAdapterEx - and I'll point out hopefully more clearly that IDependant and ResolverServiceProviderWrapper used here and before are types that only apply to my environment - if you're using a global, statically-accessible DI container, however, then it should be trivial to re-implement these two classes' CreateServiceProvider methods appropriately.

    public class ValidatableObjectAdapterEx : ValidatableObjectAdapter
    {
      public ValidatableObjectAdapterEx(ModelMetadata metadata, 
                                        ControllerContext context)
       : base(metadata, context) { }
    
      public override IEnumerable<ModelValidationResult> Validate(object container)
      {
        object model = base.Metadata.Model;
        if (model != null)
        {
          IValidatableObject instance = model as IValidatableObject;
          if (instance == null)
          {
            //the base implementation will throw an exception after 
            //doing the same check - so let's retain that behaviour
            return base.Validate(container);
          }
          /* replacement for the core functionality */
          ValidationContext validationContext = CreateValidationContext(instance);
          return this.ConvertResults(instance.Validate(validationContext));
        }
        else
          return base.Validate(container);  /*base returns an empty set 
                                              of values for null. */
      }
    
      /// <summary>
      /// Called by the Validate method to create the ValidationContext
      /// </summary>
      /// <param name="instance"></param>
      /// <returns></returns>
      protected virtual ValidationContext CreateValidationContext(object instance)
      {
        IServiceProvider serviceProvider = CreateServiceProvider(instance);
        //TODO: add virtual method perhaps for the third parameter?
        ValidationContext context = new ValidationContext(
          instance ?? Metadata.Model,
          serviceProvider,
          null);
        return context;
      }
    
      /// <summary>
      /// Called by the CreateValidationContext method to create an IServiceProvider
      /// instance to be passed to the ValidationContext.
      /// </summary>
      /// <param name="container"></param>
      /// <returns></returns>
      protected virtual IServiceProvider CreateServiceProvider(object container)
      {
        IServiceProvider serviceProvider = null;
    
        IDependant dependantController = ControllerContext.Controller as IDependant;
    
        if (dependantController != null && dependantController.Resolver != null)
        {
          serviceProvider = 
            new ResolverServiceProviderWrapper(dependantController.Resolver);
        }
        else
          serviceProvider = ControllerContext.Controller as IServiceProvider;
    
        return serviceProvider;
      }
    
      //ripped from v3 RTM source
      private IEnumerable<ModelValidationResult> ConvertResults(
        IEnumerable<ValidationResult> results)
      {
        foreach (ValidationResult result in results)
        {
          if (result != ValidationResult.Success)
          {
            if (result.MemberNames == null || !result.MemberNames.Any())
            {
              yield return new ModelValidationResult { Message = result.ErrorMessage };
            }
            else
            {
              foreach (string memberName in result.MemberNames)
              {
                yield return new ModelValidationResult 
                 { Message = result.ErrorMessage, MemberName = memberName };
              }
            }
          }
        }
      }
    }
    

    End Code

    With that class in place, you can register this as the default adapter for IValidatableObject instances with the line:

    DataAnnotationsModelValidatorProvider.
      RegisterDefaultValidatableObjectAdapterFactory(
        (metadata, context) => new ValidatableObjectAdapterEx(metadata, context)
      );
    
    0 讨论(0)
提交回复
热议问题