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

后端 未结 3 1067
被撕碎了的回忆
被撕碎了的回忆 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: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 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 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 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. */
      }
    
      /// 
      /// Called by the Validate method to create the ValidationContext
      /// 
      /// 
      /// 
      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;
      }
    
      /// 
      /// Called by the CreateValidationContext method to create an IServiceProvider
      /// instance to be passed to the ValidationContext.
      /// 
      /// 
      /// 
      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 ConvertResults(
        IEnumerable 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)
      );
    

提交回复
热议问题