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

C# Asp.Net MVC3:在ValidationContext中设置自定义IServiceProvider,以便验证器可以解析服务,c#,.net,asp.net-mvc,inversion-of-control,C#,.net,Asp.net Mvc,Inversion Of Control,2012年12月18日更新 由于这个问题似乎得到了不少意见,我应该指出,公认的答案不是我使用的解决方案,但它确实提供了构建解决方案的链接和资源,但在我看来,这不是理想的解决方案。我的答案包含对MVC框架标准部分的替换;而且,只有在您愿意检查它们是否仍适用于未来版本时,才应该使用它们(一些私有代码是从官方源代码中删除的,因为基类中没有足够的可扩展性) 但是,我可以确认,这两个类也适用于Asp.NETMVC4和Asp.NETMVC4 也可以为Asp.NETWebAPI框架重复类似的实现,这是我最近做

2012年12月18日更新

由于这个问题似乎得到了不少意见,我应该指出,公认的答案不是我使用的解决方案,但它确实提供了构建解决方案的链接和资源,但在我看来,这不是理想的解决方案。我的答案包含对MVC框架标准部分的替换;而且,只有在您愿意检查它们是否仍适用于未来版本时,才应该使用它们(一些私有代码是从官方源代码中删除的,因为基类中没有足够的可扩展性)

但是,我可以确认,这两个类也适用于Asp.NETMVC4和Asp.NETMVC4

也可以为Asp.NETWebAPI框架重复类似的实现,这是我最近做的

结束更新

我有一个类型,它有很多“标准”验证(必需等),但也有一些自定义验证

其中一些验证需要抓取服务对象,并使用其他属性之一作为键查找较低级别(即“模型层下方”)的元数据。元数据然后控制是否需要一个或多个属性以及这些属性的有效格式

更具体地说,该类型是一个卡支付对象,简化为两个相关属性,如下所示:

public class CardDetails
{
  public string CardTypeID { get; set; }
  public string CardNumber { get; set; }
}
然后我有一个服务:

public interface ICardTypeService
{
  ICardType GetCardType(string cardTypeID);
}
ICardType
则包含不同的信息位-这里的两个关键点是:

public interface ICardType
{
  //different cards support one or more card lengths
  IEnumerable<int> CardNumberLengths { get; set; }
  //e.g. - implementation of the Luhn algorithm
  Func<string, bool> CardNumberVerifier { get; set; }
}
(尽管我应该提到,此调用背后的框架是专有的)

他们通过使用公共接口获得这些信息

public interface IDependant
{
  IDependencyResolver Resolver { get; set; }
}
然后,我的框架负责在构造控制器实例时(由另一个解析器或MVC标准控制器工厂)为其分配最特定的依赖项解析器。最后一个代码块中的
Resolve
方法是这个
Resolver
成员的简单包装器

因此-如果我可以为从浏览器收到的付款获取所选的
ICardType
,那么我就可以对卡号长度等进行初始检查。问题是,如何从我的
IsValid(object,ValidationContext)
覆盖
ValidationAttribute

我需要将当前控制器的依赖项解析器传递到验证上下文。我看到
ValidationContext
都实现了
IServiceProvider
,并且有一个
IServiceContainer
的实例,所以很明显,我应该能够为我的服务解析程序创建一个包装器,它也实现了其中的一个(可能是
IServiceProvider

我已经注意到,在MVC框架生成
ValidationContext
的所有地方,服务提供者总是被传递为null

那么,在MVC管道中,我应该在什么时候覆盖核心行为并注入我的服务提供商呢


我应该补充一点,这不是我需要执行类似操作的唯一场景-因此,理想情况下,我希望我可以应用到管道中,以便使用当前控制器的当前服务提供商配置所有的
ValidationContext
s。

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

更新 除了下面显示的类之外,我还为
IValidatableObject
实现做了类似的事情(答案末尾有简短的注释,而不是完整的代码示例,因为答案太长了)-我还为该类添加了代码,以响应注释-它确实使答案很长,但至少你会有你需要的所有代码

起初的 因为我的目标是基于
ValidationAttribute
的验证,我研究了MVC在哪里创建
ValidationContext
,并将其提供给该类的
GetValidationResult
方法

原来是在
DataAnnotationsModelValidator
Validate
方法中:

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
    };
  }
}
现在,基于“普通”的ValidationAttribute的验证器可以解析服务:

public class ExampleAttribute : ValidationAttribute
{
  protected override ValidationResult 
    IsValid(object value, ValidationContext validationContext)
  {
    ICardTypeService service = 
      (ICardTypeService)validationContext.GetService(typeof(ICardTypeService));
  }
}
这仍然需要重新实现direct
ModelValidator
,以支持相同的技术,尽管他们已经可以访问
ControllerContext
,所以问题不大

更新 如果您希望
IValidatableObject
-实现类型能够在
Validate
实现期间解析服务,而不必为每种类型派生自己的适配器,则必须执行类似的操作

  • ValidatableObjectAdapter
    派生一个新类,我称之为
    ValidatableObjectAdapterEx
  • 从MVCs v3 RTM源代码中,复制该类的
    Validate
    ConvertResults
    私有方法
  • 调整第一个方法以删除对内部MVC资源的引用,以及
  • 更改
    ValidationContext
    的构造方式
更新(回应以下评论) 下面是
ValidatableObjectAdapterEx
的代码,我希望能更清楚地指出,这里和之前使用的
IDependant
ResolverServiceProviderWrapper
都是仅适用于我的环境的类型-但是,如果您使用的是全局的、静态可访问的DI容器,然后,重新实现这两个类“
CreateServiceProvider”应该很简单
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;
  }
}
//register the new factory over the top of the standard one.
DataAnnotationsModelValidatorProvider.RegisterDefaultAdapterFactory(
  (metadata, context, attribute) => 
    new DataAnnotationsModelValidatorEx(metadata, context, attribute));
public class ExampleAttribute : ValidationAttribute
{
  protected override ValidationResult 
    IsValid(object value, ValidationContext validationContext)
  {
    ICardTypeService service = 
      (ICardTypeService)validationContext.GetService(typeof(ICardTypeService));
  }
}
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 };
          }
        }
      }
    }
  }
}
DataAnnotationsModelValidatorProvider.
  RegisterDefaultValidatableObjectAdapterFactory(
    (metadata, context) => new ValidatableObjectAdapterEx(metadata, context)
  );
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;
        }
    }
}
#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>();
}
DataAnnotationsModelValidatorProvider.RegisterAdapterFactory(
    typeof(ValidatorServiceAttribute),
    (metadata, context, attribute) => new DataAnnotationsModelValidatorEx(metadata, context, attribute, true));
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);
    }
}