Asp.Net MVC3: establezca IServiceProvider personalizado en ValidationContext para que los validadores puedan resolver los servicios

Actualización 18 de diciembre de 2012

Dado que esta pregunta parece obtener bastantes puntos de vista, debo señalar que la respuesta aceptada no es la solución que utilicé, pero proporciona los enlaces y los recursos para construir una solución, pero, en mi opinión, no es la solución ideal. . Mi respuesta contiene reemplazos para partes estándar del marco MVC; y solo debería usarlos si se siente cómodo comprobando que todavía funcionan para versiones futuras (se extrajo código privado de las fonts oficiales, porque no había suficiente extensibilidad en las clases base).

Puedo confirmar, sin embargo, que estas dos clases también funcionan para Asp.Net MVC 4 y 3.

También es posible repetir una implementación similar para Asp.Net Web API Framework, lo cual hice recientemente.

Fin de actualización

Tengo un tipo que tiene mucha validación ‘estándar’ (requerida, etc.) pero también un poco de validación personalizada.

Parte de esta validación requiere agarrar un objeto de servicio y buscar algunos metadatos de nivel inferior (es decir, “debajo” de la capa de modelo) utilizando una de las otras propiedades como una clave. Los metadatos entonces controlan si se requieren una o más propiedades, así como también formatos válidos para esas propiedades.

Para ser más concreto: el tipo es un objeto de Pago de Tarjeta, simplificado a dos de las propiedades en cuestión de la siguiente manera:

public class CardDetails { public string CardTypeID { get; set; } public string CardNumber { get; set; } } 

Entonces tengo un servicio:

 public interface ICardTypeService { ICardType GetCardType(string cardTypeID); } 

ICardType contiene diferentes partes de la información, las dos aquí son cruciales:

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

Todos mis controladores tienen la capacidad de resolver un ICardTypeService usando un patrón estándar, es decir,

  var service = Resolve(); 

(Aunque debo mencionar que el marco detrás de esta llamada es propietario)

Que ganan mediante el uso de una interfaz común

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

Mi framework se encarga entonces de asignar el resolvedor de dependencias más específico disponible para la instancia del controlador cuando está construido (ya sea por otro resolver o por la fábrica de controladores estándar MVC). El método Resolve en el último bloque de código, pero uno, es una envoltura simple alrededor de este miembro de Resolver .

Entonces, si puedo tomar el ICardType seleccionado para el pago que se recibe del navegador, entonces puedo realizar verificaciones iniciales de la longitud del número de tarjeta, etc. El problema es cómo resolver el servicio desde mi sobreescritura de IsValid(object, ValidationContext) anulación de ValidationAttribute ?

Necesito pasar el resolvedor de dependencias del controlador actual al contexto de validación. Veo que ValidationContext implementa IServiceProvider y tiene una instancia de IServiceContainer , así que claramente debería ser capaz de crear un contenedor para mi servicio de resolución que también implemente uno de ellos (probablemente IServiceProvider ).

Ya he notado que en todos los lugares donde MVC framework produce un ValidationContext , el proveedor de servicios siempre pasa nulo.

Entonces, ¿en qué punto de la tubería de MVC debería estar buscando anular el comportamiento central e inyectar a mi proveedor de servicios?

Debo añadir que este no será el único escenario en el que tengo que hacer algo como esto, así que idealmente me gustaría algo que pueda aplicar a la canalización para que todos los ValidationContext estén configurados con el proveedor de servicios actual para el actual controlador.

¿Ha pensado en crear un modelo de validador, utilizando un modelValidatorProvider, en lugar de usar atributos de validación? De esta manera, no depende de ValidationAttribute, pero puede crear su propia implementación de validación (esto funcionará además de la validación de DataAnnotations existente).

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

Actualizar

Además de la clase que se muestra a continuación, también hice algo similar para IValidatableObject implementaciones de IValidatableObject (notas cortas al final de la respuesta en lugar de una muestra de código completo porque la respuesta es demasiado larga) – He agregado el código para esa clase también en respuesta a un comentario; hace que la respuesta sea muy larga, pero al menos tendrá todo el código que necesita.

Original

Ya que estoy apuntando a la ValidationAttribute basada en ValidationAttribute en el momento en que investigué donde MVC crea el ValidationContext que se alimenta al método GetValidationResult de esa clase.

Resulta que está en el método Validate DataAnnotationsModelValidator :

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

(Copiado y reformateado desde MVC3 RTM Source)

Así que pensé que alguna extensibilidad aquí estaría en orden:

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

Así que primero IDependant mi interfaz IDependant desde el controlador, en cuyo caso creo una instancia de una clase contenedora que actúa como un adaptador entre mi interfaz IDependencyResolver y System.IServiceProvider .

Pensé que también manejaría casos en los que un controlador también es un IServiceProvider (no es lo que se aplica en mi caso, pero es una solución más general).

Luego hago que DataAnnotationsModelValidatorProvider use este validador de forma predeterminada, en lugar del original:

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

Ahora los validadores basados ​​en ValidationAttribute ‘normales’ pueden resolver los servicios:

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

Esto todavía deja a Direct ModelValidator que se debe volver a implementar para admitir la misma técnica, aunque ya tienen acceso al ControllerContext , por lo que no es un problema.

Actualizar

Algo similar debe hacerse si desea que IValidatableObject -implementing types pueda resolver servicios durante la implementación de Validate sin tener que seguir derivando sus propios adaptadores para cada tipo.

  • Derive una nueva clase de ValidatableObjectAdapter , lo llamé ValidatableObjectAdapterEx
  • de la fuente MVCs v3 RTM, copie el método privado Validate and ConvertResults de esa clase.
  • Ajuste el primer método para eliminar las referencias a los recursos MVC internos, y
  • cambiar cómo se construye el ValidationContext

Actualización (en respuesta a los comentarios a continuación)

Aquí está el código para ValidatableObjectAdapterEx , y señalaré con un poco de suerte que IDependant y ResolverServiceProviderWrapper utilizados aquí y antes son tipos que solo se aplican a mi entorno; sin embargo, si está utilizando un contenedor DI global, accesible de forma estática, entonces debería ser trivial volver a implementar los métodos CreateServiceProvider estas dos clases de CreateServiceProvider adecuada.

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

Código final

Con esa clase en su lugar, puede registrar esto como el adaptador predeterminado para IValidatableObject instancias de IValidatableObject con la línea:

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

En MVC 5.2, puede aprovechar la respuesta de robo @ Andras y la fuente de MVC y:

1. Derive un DataAnnotationsModelValidatorEx de 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. Clonar la impl base de public override IEnumerable Validate(object container)

3. Aplicar el truco Renderice la incisión elegante después de Validate crea el contexto: –

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. Informe a MVC sobre el nuevo DataAnnotationsModelValidatorProvider en la ciudad

después de que Global.asax haga DependencyResolver.SetResolver(new AutofacDependencyResolver(container)) : –

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

5. Utilice su imaginación para abusar de su nuevo consumidor de Localizador de servicios utilizando inyección de ctor a través de GetService en su ValidationAttribute , por ejemplo:

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

Te permite darle una bofetada a tu modelo: –

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

que lo conecta a:

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

Los dos anteriores están conectados por:

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

Estoy abierto a las correcciones, pero sobre todo, al equipo de ASP.NET, ¿estaría abierto a un RP para agregar un constructor con este DataAnnotationsModelValidator a DataAnnotationsModelValidator ?