Asp.Net MVC3: imposta IServiceProvider personalizzato in ValidationContext in modo che i validatori possano risolvere i servizi

Aggiornamento del 18 dicembre 2012

Dato che questa domanda sembra avere parecchie opinioni, devo sottolineare che la risposta accettata non è la soluzione che ho usato, ma fornisce i collegamenti e le risorse per build una soluzione, ma, a mio parere, non è la soluzione ideale . La mia risposta contiene sostituzioni per parti standard del framework MVC; e dovresti usarli solo se sei a tuo agio nel controllare che funzionino ancora per le versioni future (alcuni codici privati ​​sono stati estratti dalle fonti ufficiali, perché non c’era abbastanza estensibilità nelle classi base).

Posso confermare, tuttavia, che queste due classi funzionano anche per Asp.Net MVC 4 e 3.

È anche ansible ripetere un’implementazione simile anche per il framework API Web Asp.Net, cosa che ho fatto di recente.

Fine aggiornamento

Ho un tipo che ha un sacco di convalida “standard” (richiesta, ecc.) Ma anche un po ‘di validazione personalizzata.

Alcune di queste convalide richiedono l’acquisizione di un object di servizio e la ricerca di alcuni metadati di livello inferiore (cioè “sotto” il livello del modello) utilizzando una delle altre proprietà come chiave. I metadati controllano quindi se sono richieste una o più proprietà e formati validi per tali proprietà.

Per essere più concreti, il tipo è un object Pagamento carta, semplificato in due delle proprietà in questione come segue:

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

Allora ho un servizio:

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

ICardType contiene quindi diversi bit di informazioni: i due qui che sono cruciali sono:

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

I miei controller hanno tutti la capacità di risolvere un ICardTypeService usando un modello standard, ad es

  var service = Resolve(); 

(Anche se dovrei menzionare che il framework dietro questa chiamata è proprietario)

Che ottengono tramite l’uso di un’interfaccia comune

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

Il mio framework si occupa quindi dell’assegnazione del resolver di dipendenze più specifico disponibile per l’istanza del controller quando è costruita (da un altro resolver o dalla factory del controller standard MVC). Il metodo Resolve nell’ultimo ma un blocco di codice è un semplice wrapper attorno a questo membro Resolver .

Quindi, se riesco ad afferrare l’ ICardType selezionato per il pagamento ricevuto dal browser, posso quindi eseguire i controlli iniziali sulla lunghezza del numero di carta ecc. Il problema è, come risolvere il servizio dall’interno della mia sovrascrittura di IsValid(object, ValidationContext) override di ValidationAttribute ?

Ho bisogno di passare attraverso il resolver di dipendenza del controller corrente al contesto di validazione. Vedo che ValidationContext implementa IServiceProvider e ha un’istanza di IServiceContainer , quindi dovrei essere in grado di creare un wrapper per il mio risolutore di servizio che implementa anche uno di questi (probabilmente IServiceProvider ).

Ho già notato che in tutti i posti in cui un ValidationContext è prodotto dal framework MVC, il fornitore di servizi viene sempre passato nullo.

Quindi a che punto della pipeline MVC dovrei cercare di ignorare il comportamento di base e iniettare il mio fornitore di servizi?

Devo aggiungere che questo non sarà l’unico scenario in cui ho bisogno di fare qualcosa di simile – quindi idealmente vorrei qualcosa che posso applicare alla pipeline in modo che tutti i ValidationContext siano configurati con il fornitore di servizi corrente per l’attuale controller.

Hai pensato di creare un validatore di modello, utilizzando un modelValidatorProvider, invece di utilizzare gli attributi di convalida? In questo modo non sei dipendente da ValidationAttribute ma puoi creare la tua implementazione di convalida (questo funzionerà in aggiunta alla convalida DataAnnotations esistente).

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

Aggiornare

Oltre alla class mostrata di seguito, ho fatto una cosa simile anche per IValidatableObject implementazioni IValidatableObject (note brevi verso la fine della risposta invece di un codice completo, perché la risposta diventa troppo lunga) – Ho aggiunto il codice per quella class e in risposta ad un commento – rende la risposta molto lunga, ma almeno avrai tutto il codice che ti serve.

Originale

Poiché ho GetValidationResult metodo di convalida ValidationAttribute , al momento ho GetValidationResult dove MVC crea il ValidationContext che viene alimentato con il metodo GetValidationResult di quella class.

Risulta che è nel metodo 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 }; } } 

(Copiato e riformattato da MVC3 RTM Source)

Quindi ho pensato che l’estensibilità qui sarebbe in ordine:

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

Quindi prima controllo la mia interfaccia IDependant dal controller, nel qual caso creo un’istanza di una class wrapper che funge da adattatore tra la mia interfaccia IDependencyResolver e System.IServiceProvider .

Ho pensato di gestire anche casi in cui un controller stesso è anche un IServiceProvider (non che ciò si applica nel mio caso, ma è una soluzione più generale).

Quindi faccio in modo che DataAnnotationsModelValidatorProvider utilizzi questo validatore per impostazione predefinita, anziché l’originale:

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

Ora i validatori “normali” ValidationAttribute possono risolvere i servizi:

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

Questo lascia comunque il ModelValidator diretto che deve essere reimplementato per supportare la stessa tecnica, sebbene abbia già accesso a ControllerContext , quindi è meno di un problema.

Aggiornare

Una cosa simile deve essere eseguita se si desidera che i tipi di IValidatableObject IValidatableObject siano in grado di risolvere i servizi durante l’implementazione di Validate senza dover continuare a ricavare i propri adattatori per ciascun tipo.

  • Ricavare una nuova class da ValidatableObjectAdapter , l’ho chiamata ValidatableObjectAdapterEx
  • dall’origine RTM di MVCs v3, copiare il metodo privato Validate e ConvertResults di quella class.
  • Regola il primo metodo per rimuovere i riferimenti alle risorse MVC interne e
  • cambia come viene costruito ValidationContext

Aggiornamento (in risposta al commento di seguito)

Ecco il codice per ValidatableObjectAdapterEx – e IDependant maggior chiarezza che IDependant e ResolverServiceProviderWrapper usati qui e prima sono tipi che si applicano solo al mio ambiente – se si sta utilizzando un contenitore DI, accessibile a livello statico, comunque, quindi, dovrebbe essere banale implementare appropriatamente i due metodi CreateServiceProvider due classi.

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

Codice finale

Con quella class in atto, puoi registrarlo come l’adattatore predefinito per IValidatableObject istanze IValidatableObject con la linea:

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

Su MVC 5.2, puoi sfruttare la risposta di steal @ Andras e la fonte MVC e:

1. DataAnnotationsModelValidatorEx un DataAnnotationsModelValidatorEx da 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. Clona l’impl di base di public override IEnumerable Validate(object container)

3. Applica la modifica Rendi l’elegante incisione dopo che Validate crea il contesto: –

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. Comunicare a MVC il nuovo DataAnnotationsModelValidatorProvider in città

dopo che Global.asax fa DependencyResolver.SetResolver(new AutofacDependencyResolver(container)) : –

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

5. Usa la tua immaginazione per abusare del consumo del tuo nuovo Service Locator usando l’iniezione di ctor tramite GetService nel tuo ValidationAttribute , ad esempio:

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

Ti permette di schiaffarlo sul tuo modello: –

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

che lo collega 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); } } 

I due precedenti sono collegati da:

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

Sono aperto a correzioni, ma soprattutto, il team di ASP.NET, saresti aperto a un PR per aggiungere un costruttore con questa funzionalità a DataAnnotationsModelValidator ?