问题 Asp.Net MVC3:在ValidationContext中设置自定义IServiceProvider,以便验证器可以解析服务


2012年12月18日更新


2289
2018-03-07 10:10


起源



答案:


您是否考虑过使用modelValidatorProvider创建模型验证器,而不是使用验证属性?这样您就不依赖于ValidationAttribute,但可以创建自己的验证实现(这将与现有的DataAnnotations验证一起使用)。

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


3
2018-03-07 14:14



那里有一些好文章,谢谢;而且我当然可以通过这种方式接近 - 但是这里有一些比我真正喜欢的全能解决方案更多。同样,我喜欢基于属性的验证,因为它非常明显(特别是对于我的团队中的其他开发人员来说,目前还不如MVC那么舒服 - 不是我说特别是要求)。当然可以这样做,所以+1。 - Andras Zoltan
您仍然可以使用自定义模型验证程序提供程序实现基于属性的验证。 DataAnnotations以相同的方式实现。您不必使用与DataAnnotations相同的方法。 - Linkgoron
我发现相当低的影响方式(当我确认它有效时我会发布它!)使它适用于所有不使用自己的适配器的基于属性的验证器。在你发布你的答案之前我已经在研究它 - 只是不想跳枪:);是否符合社区批准还有待观察! - Andras Zoltan
发布完整解决方案 - Andras Zoltan
将此标记为答案,因为我的解决方案在升级到MVC的未来版本时需要精力充沛,MVC只应由开发人员采用,以便重新管理MVC的低级别位。 - Andras Zoltan


答案:


您是否考虑过使用modelValidatorProvider创建模型验证器,而不是使用验证属性?这样您就不依赖于ValidationAttribute,但可以创建自己的验证实现(这将与现有的DataAnnotations验证一起使用)。

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


3
2018-03-07 14:14



那里有一些好文章,谢谢;而且我当然可以通过这种方式接近 - 但是这里有一些比我真正喜欢的全能解决方案更多。同样,我喜欢基于属性的验证,因为它非常明显(特别是对于我的团队中的其他开发人员来说,目前还不如MVC那么舒服 - 不是我说特别是要求)。当然可以这样做,所以+1。 - Andras Zoltan
您仍然可以使用自定义模型验证程序提供程序实现基于属性的验证。 DataAnnotations以相同的方式实现。您不必使用与DataAnnotations相同的方法。 - Linkgoron
我发现相当低的影响方式(当我确认它有效时我会发布它!)使它适用于所有不使用自己的适配器的基于属性的验证器。在你发布你的答案之前我已经在研究它 - 只是不想跳枪:);是否符合社区批准还有待观察! - Andras Zoltan
发布完整解决方案 - Andras Zoltan
将此标记为答案,因为我的解决方案在升级到MVC的未来版本时需要精力充沛,MVC只应由开发人员采用,以便重新管理MVC的低级别位。 - Andras Zoltan


更新

除了下面显示的类之外,我也做了类似的事情 IValidatableObject 实现也是如此 (在答案结束时的简短说明而不是完整的代码示例,因为那时答案太长了)  - 我已经添加了该类的代码以响应评论 - 它确实得到了答案 非常 很长,但至少你会得到你需要的所有代码。

原版的

因为我的目标 ValidationAttribute在我研究MVC创建的地方的那一刻,我进行了验证 ValidationContext 这得到了 GetValidationResult 该类的方法。

事实证明它在 DataAnnotationsModelValidatorValidate 方法:

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

(从MVC3 RTM源复制并重新格式化)

所以我认为这里的一些可扩展性将是有序的:

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

所以我先检查一下 IDependant 来自控制器的接口,在这种情况下,我创建一个包装类的实例,充当我之间的适配器 IDependencyResolver 界面和 System.IServiceProvider

我以为我也会处理控制器本身的情况 IServiceProvider 也是(在我的情况下不适用 - 但这是一个更通用的解决方案)。

然后我做了 DataAnnotationsModelValidatorProvider 默认使用此验证器,而不是原始验证器:

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

现在'正常' ValidationAttribute基于验证器,可以解决服务:

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

这仍然是直接的 ModelValidator - 需要重新实现以支持相同的技术 - 虽然他们已经可以访问 ControllerContext,所以这不是一个问题。

更新

如果你愿意,还有类似的事情要做 IValidatableObject - 实现类型,以便在执行期间解析服务 Validate 无需为每种类型继续派生自己的适配器。

  • 从中派生一个新类 ValidatableObjectAdapter我打电话给它 ValidatableObjectAdapterEx
  • 来自MVC v3 RTM源码,复制 Validate 和 ConvertResults 该类的私有方法。
  • 调整第一种方法以删除对内部MVC资源的引用,以及
  • 怎么改变 ValidationContext 是建造的

更新(回应下面的评论)

这是代码 ValidatableObjectAdapterEx  - 我希望能更清楚地指出这一点 IDependant 和 ResolverServiceProviderWrapper 这里和之前使用的类型只适用于我的环境 - 如果你使用的是一个静态可访问的全局DI容器,那么重新实现这两个类应该是微不足道的 CreateServiceProvider方法恰当。

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

结束代码

使用该类,您可以将其注册为默认适配器 IValidatableObject 行的实例:

DataAnnotationsModelValidatorProvider.
  RegisterDefaultValidatableObjectAdapterFactory(
    (metadata, context) => new ValidatableObjectAdapterEx(metadata, context)
  );

4
2018-03-07 16:32



只要您不必为每种具体类型更改ModelValidator,我认为这是有效的。 - Linkgoron
是的,现在已经开始在几种类型上推出它,工作正常!然后是您指向其他验证的解决方案。涵盖所有基地,就像我喜欢的方式:) - Andras Zoltan
我只是添加它 - >如果你需要从父类复制和粘贴私有方法它 威力 意味着你正在做一些你不应该做的事情。 - Linkgoron
大声笑 - 我知道这一点;虽然它 威力 同样意味着框架中缺少可扩展性点......在这种情况下,除非您为验证器需要它们的每个类型创建并注册特殊验证器,否则在MVC验证期间没有其他方法可以利用服务提供者。其中每一个也可能包含相同的代码或基类 - 所以为什么不将它作为默认行为。 - Andras Zoltan
刚刚做了一个相同的实现。我希望微软能够选择为MVC4和DependencyResolver开箱即用。 IServiceProvider共享相同的签名,因此唯一需要更改的是使IDependencyResolver实现IServiceProvider,并在创建ValidationContext时传递它。 - MartinF


在MVC 5.2上,你可以 杠杆作用@Andras的回答 和MVC来源和:

1.导出一个 DataAnnotationsModelValidatorEx 从 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.克隆基础impl public override IEnumerable<ModelValidationResult> Validate(object container)

3。 应用黑客 之后呈现优雅的切口 Validate 创建上下文: -

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

告诉MVC新的 DataAnnotationsModelValidatorProvider 城里

在您的Global.asax之后 DependencyResolver.SetResolver(new AutofacDependencyResolver(container)) : -

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

用你的想象力 滥用您的新服务定位器 通过使用ctor注射消耗 GetService 在你的 ValidationAttribute, 例如:

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

允许你在你的模特上拍它: -

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

将它连接到:

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

前两者通过以下方式连接:

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

我愿意接受更正,但最重要的是,ASP.NET团队,您是否愿意接受PR来添加具有此功能的构造函数 DataAnnotationsModelValidator


4
2018-02-05 00:45