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

2012年12月18日更新

由于这个问题似乎得到了不少观点,我应该指出,接受的答案不是我使用的解决方案,但它确实提供了构建解决方案的链接和资源,但是,在我看来,不是理想的解决方案。 我的回答包含 MVC框架标准部分的 替换 ; 并且你应该只使用那些,如果你觉得它们仍然适用于未来的版本(一些私有代码被从官方资源中删除,因为基类中没有足够的可扩展性)。

但是,我可以确认这两个类也适用于Asp.Net MVC 4以及3。

我也可以重复Asp.Net Web API框架的类似实现,这是我最近所做的。

结束更新

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

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

更具体一点 – 类型是卡片支付对象,简化为两个有问题的属性,如下所示:

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 CardNumberLengths { get; set; } //eg - implementation of the Luhn algorithm Func CardNumberVerifier { get; set; } } 

我的控制器都能够使用标准模式解析ICardTypeService

  var service = Resolve(); 

(虽然我应该提一下这个电话背后的框架是专有的)

他们通过使用通用接口获得了什么

 public interface IDependant { IDependencyResolver Resolver { get; set; } } 

然后,我的框架负责在构造控制器实例时(由另一个解析器或MVC标准控制器工厂)分​​配可用于控制器实例的最具体的依赖项解析器。 最后一个代码块中的Resolve方法是围绕此Resolver成员的简单包装Resolver

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

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

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

那么在MVC管道中的哪一点我应该寻求覆盖核心行为并注入我的服务提供者?

我应该补充一点,这不是我需要做这样的事情的唯一场景 – 所以理想情况下我想要一些我可以应用于管道的东西,以便所有ValidationContext都配置当前的服务提供者用于当前控制器。

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

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

更新

除了下面显示的类之外,我也为IValidatableObject实现做了类似的事情(在答案结束时的简短注释而不是完整的代码示例,因为那时答案太长了) – 我添加了该类的代码以及对注释的响应 – 它确实使答案长,但至少你将拥有所需的所有代码。

原版的

由于我目前正在研究基于ValidationAttribute的validation,因此我研究了MVC创建ValidationContext ,该ValidationContext被提供给GetValidationResult方法。

原来它在DataAnnotationsModelValidatorValidate方法中:

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

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

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

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

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

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

然后我使DataAnnotationsModelValidatorProvider默认使用此validation器,而不是原始validation器:

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

现在’普通’基于ValidationAttribute的validation器,可以解析服务:

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

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

更新

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

  • ValidatableObjectAdapter派生一个新类,我称之为ValidatableObjectAdapterEx
  • 从MVC v3 RTM源,复制ConvertResultsValidateConvertResults私有方法。
  • 调整第一种方法以删除对内部MVC资源的引用,以及
  • 更改ValidationContext的构造方式

更新(回应下面的评论)

这是ValidatableObjectAdapterEx的代码 – 我希望更清楚地指出,此处和之前使用的IDependantResolverServiceProviderWrapper是仅适用于我的环境的类型 – 如果您使用的是全局的,静态可访问的DI容器,那么适当地重新实现这两个类的CreateServiceProvider方法应该是微不足道的。

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

结束代码

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

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

在MVC 5.2上,您可以利用窃取@Andras的答案和MVC源代码并:

1.从DataAnnotationsModelValidator派生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.克隆public override IEnumerable Validate(object container)的基础impl public override IEnumerable Validate(object container)

3. 应用hackValidate创建上下文后渲染优雅的切口: –

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 (eg 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.告诉MVC关于城镇中新的DataAnnotationsModelValidatorProvider

在您的Global.asax执行DependencyResolver.SetResolver(new AutofacDependencyResolver(container)) : –

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

5.通过ValidationAttribute GetService使用ctor注入来利用您的想象来滥用您的新服务定位器 ,例如:

 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 { 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 : 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添加具有此function的构造函数?