12

In MVC, posso creare un validatore del modello che può assumere dipendenze. Normalmente utilizzo FluentValidation per questo. Questo mi permette di, per esempio, verifico registrazione conto che un indirizzo e-mail non è stata utilizzata (NB: Questo è un esempio semplificato!): EsisteConvalida iniettata dalla dipendenza nell'API Web

public class RegisterModelValidator : AbstractValidator<RegisterModel> { 
    private readonly MyContext _context; 
    public RegisterModelValidator(MyContext context) { 
     _context = context; 
    } 
    public override ValidationResult Validate(ValidationContext<RegisterModel> context) { 
     var result = base.Validate(context); 
     if (context.Accounts.Any(acc => acc.Email == context.InstanceToValidate.Email)){ 
      result.Errors.Add(new ValidationFailure("Email", "Email has been used")); 
     } 
     return result; 
    } 
} 

Nessun tale integrazione per l'API Web con FluentValidation . Ci sono stati uno couple di attempts a questo, ma nessuno dei due ha affrontato l'aspetto di Dependency Injection e funziona solo con validatori statici.

Il motivo per cui questo è difficile è dovuto alla diversa implementazione di ModelValidatorProvider e ModelValidator tra MVC e Web API. In MVC, questi sono istanziati per richiesta (quindi l'iniezione di un contesto è semplice). In Web API, sono statici e ModelValidatorProvider mantiene una cache di ModelValidators per tipo, per evitare inutili ricerche di riflessioni su ogni richiesta.

Ho cercato di aggiungere l'integrazione necessaria, ma sono stato stuck trying to obtain the Dependency Scope. Invece, ho pensato di fare un passo indietro e chiedere se ci sono altre soluzioni al problema - se qualcuno ha escogitato una soluzione per eseguire la validazione del modello in cui le dipendenze possono essere iniettate.

NON voglio eseguire la convalida all'interno del Controller (sto utilizzando un ValidationActionFilter per tenerlo separato), il che significa che non posso ottenere alcun aiuto dall'iniezione del costruttore del controller.

+0

Questa è una domanda eccellente in quanto sembra che tu abbia davvero fatto la tua ricerca prima di chiedere.Non sono abbastanza sicuro che al momento in cui è stata posta questa domanda tu stavi davvero lottando con l'essere in grado di iniettare dipendenze, o se il problema era che a causa della memorizzazione nella cache che si verificava che qualsiasi dipendenza iniettata non sarebbe stata risolta ogni volta la validazione viene eseguita. Questa è la situazione in cui si è scontrata la mia squadra per la quale ho aperto un problema con FluentValidation: https://github.com/JeremySkinner/FluentValidation/issues/108. La tua soluzione qui sotto potrebbe essere una soluzione praticabile per noi. –

+0

Li ho appena messi su nuget se si desidera utilizzarli – Richard

risposta

4

ho finalmente questo lavoro, ma è un po 'un bodge. Come accennato in precedenza, ModelValidatorProvider manterrà le istanze Singleton di tutti i Validator in giro, quindi questo era completamente inadatto. Invece, sto usando un filtro per eseguire la mia convalida, come suggerito da Oppositional. Questo filtro ha accesso allo IDependencyScope e può istanziare ordinatamente i validatori.

All'interno del filtro, passo attraverso il ActionArguments e li passo attraverso la convalida.Il codice di convalida è stato copiato dall'origine di runtime API Web per DefaultBodyModelValidator, modificato per cercare il Validator all'interno di DependencyScope.

Infine, per fare questo lavoro con l'ValidationActionFilter, è necessario ensure that your filters are executed in a specific order.

Ho confezionato la mia soluzione su github, con una versione accessibile NuGet.

+0

Richard, hai mai preso questo pacchetto? –

+1

Il codice è attivo su github all'indirizzo https://github.com/richardlawley/WebApi-FluentValidation e https://github.com/richardlawley/WebApi-OrderedFilters – Richard

3

Sono stato in grado di registrare e quindi accedere al resolver di dipendenze API Web dalla richiesta utilizzando il metodo di estensione GetDependencyScope(). Ciò consente l'accesso al validatore del modello quando il filtro di convalida è in esecuzione.

Non esitare a chiarire se questo non risolve i problemi relativi all'iniezione di dipendenza.

Configuration Web API (con Unity come il contenitore IoC):

public static void Register(HttpConfiguration config) 
{ 
    config.DependencyResolver = new UnityDependencyResolver(
     new UnityContainer() 
     .RegisterInstance<MyContext>(new MyContext()) 
     .RegisterType<AccountValidator>() 

     .RegisterType<Controllers.AccountsController>() 
    ); 

    config.Routes.MapHttpRoute(
     name:   "DefaultApi", 
     routeTemplate: "api/{controller}/{id}", 
     defaults:  new { id = RouteParameter.Optional } 
    ); 
} 

convalida filtro azione:

public class ModelValidationFilterAttribute : ActionFilterAttribute 
{ 
    public ModelValidationFilterAttribute() : base() 
    { 
    } 

    public override void OnActionExecuting(HttpActionContext actionContext) 
    { 
     var scope = actionContext.Request.GetDependencyScope(); 

     if (scope != null) 
     { 
      var validator = scope.GetService(typeof(AccountValidator)) as AccountValidator; 

      // validate request using validator here... 
     } 

     base.OnActionExecuting(actionContext); 
    } 
} 

Modello Validator:

public class AccountValidator : AbstractValidator<Account> 
{ 
    private readonly MyContext _context; 

    public AccountValidator(MyContext context) : base() 
    { 
     _context = context; 
    } 

    public override ValidationResult Validate(ValidationContext<Account> context) 
    { 
     var result  = base.Validate(context); 
     var resource = context.InstanceToValidate; 

     if (_context.Accounts.Any(account => String.Equals(account.EmailAddress, resource.EmailAddress))) 
     { 
      result.Errors.Add(
       new ValidationFailure("EmailAddress", String.Format("An account with an email address of '{0}' already exists.", resource.EmailAddress)) 
      ); 
     } 

     return result; 
    } 
} 

API Action Controller Metodo:

[HttpPost(), ModelValidationFilter()] 
public HttpResponseMessage Post(Account account) 
{ 
    var scope = this.Request.GetDependencyScope(); 

    if(scope != null) 
    { 
     var accountContext = scope.GetService(typeof(MyContext)) as MyContext; 
     accountContext.Accounts.Add(account); 
    } 

    return this.Request.CreateResponse(HttpStatusCode.Created); 
} 

Model (Esempio):

public class Account 
{ 
    public Account() 
    { 
    } 

    public string FirstName 
    { 
     get; 
     set; 
    } 

    public string LastName 
    { 
     get; 
     set; 
    } 

    public string EmailAddress 
    { 
     get; 
     set; 
    } 
} 

public class MyContext 
{ 
    public MyContext() 
    { 
    } 

    public List<Account> Accounts 
    { 
     get 
     { 
      return _accounts; 
     } 
    } 
    private readonly List<Account> _accounts = new List<Account>(); 
} 
+0

Sfortunatamente, la convalida dell'API Web è già stata richiamata in questa fase, quindi non è possibile farlo con la normale convalida, che è quello che speravo fare. Tuttavia, sembra che potrebbe essere possibile fare un secondo giro di convalida in questo modo, che è un po 'un bodge ma meglio di niente! Ci provo. – Richard

+0

Sono decisamente interessato a sapere come lo risolvete, poiché vorrei fare qualcosa di simile, quindi postate la vostra soluzione se avete tempo. – Oppositional

+0

Non sono riuscito a farlo funzionare venerdì, soprattutto perché non si ha accesso al modello correttamente dal filtro. Tuttavia, provando altre idee relative a questo oggi. – Richard

-1

FluentValidation ha avuto il supporto per WebAPI per parecchio tempo (non so se le date di domanda prima che): https://fluentvalidation.codeplex.com/discussions/533373

Citando dal thread:

{ 
    GlobalConfiguration.Configuration.Services.Add(typeof(System.Web.Http.Validation.ModelValidatorProvider), 
     new WebApiFluentValidationModelValidatorProvider() 
     { 
      AddImplicitRequiredValidator = false //we need this otherwise it invalidates all not passed fields(through json). btw do it if you need 
     }); 
     FluentValidation.ValidatorOptions.ResourceProviderType = typeof(FluentValidationMessages); // if you have any related resource file (resx) 
     FluentValidation.ValidatorOptions.CascadeMode = FluentValidation.CascadeMode.Continue; //if you need! 

ho utilizzato nel progetto WebApi2 senza qualsiasi problema

+0

La domanda non riguarda l'utilizzo di FluentValidation in Web API, si tratta di utilizzare in congiunzione con Dependency Injection, che non può essere eseguita nell'implementazione standard, a causa del fatto che i validatori sono statici. – Richard

+0

Scusate il mio male ... Stavo leggendolo in un contesto diff. Non sono sicuro che il mio progetto abbia qualche validatore che prenda il comando ora, ma controllerò e tornerò. dal suono di esso, penso che probabilmente devo cancellare la mia risposta ...: | – Mrchief

1

Ho DI che funziona con i validatori di flusso in WebApi senza problemi. Ho scoperto che i validatori vengono chiamati molto, e questo tipo di convalide logiche pesanti non ha posto in un validatore di modelli. I validatori di modelli, a mio parere, sono pensati per essere leggeri controllando la forma dei dati. Il numero Email sembra un messaggio di posta elettronica e il chiamante ha fornito FirstName, LastName e Mobile o HomePhone?

Convalida logica come Questa email può essere registrata appartiene al livello di servizio, non a un controller. Anche le mie implementazioni non condividono un contesto di dati implicito poiché penso che sia un anti-modello.

Penso che l'attuale pacchetto NuGet abbia una dipendenza MVC3, quindi ho finito con lo source directly e ho creato il mio NinjectFluentValidatorFactory.

Nel App_Start/NinjectWebCommon.cs abbiamo il seguente.

/// <summary> 
    /// Set up Fluent Validation for WebApi. 
    /// </summary> 
    private static void FluentValidationSetup(IKernel kernel) 
    { 
     var ninjectValidatorFactory 
         = new NinjectFluentValidatorFactory(kernel); 

     // Configure MVC 
     FluentValidation.Mvc.FluentValidationModelValidatorProvider.Configure(
      provider => provider.ValidatorFactory = ninjectValidatorFactory); 

     // Configure WebApi 
     FluentValidation.WebApi.FluentValidationModelValidatorProvider.Configure(
      System.Web.Http.GlobalConfiguration.Configuration, 
      provider => provider.ValidatorFactory = ninjectValidatorFactory); 

     DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false; 
    } 

Credo che l'unico altri pacchetti richiesti per quanto sopra sono:

<package id="FluentValidation" version="5.1.0.0" targetFramework="net451" /> 
    <package id="FluentValidation.MVC5" version="5.1.0.0" targetFramework="net451" /> 
    <package id="FluentValidation.WebApi" version="5.1.0.0" targetFramework="net451" /> 
    <package id="Ninject" version="3.2.0.0" targetFramework="net451" /> 
    <package id="Ninject.MVC3" version="3.2.0.0" targetFramework="net451" /> 
    <package id="Ninject.Web.Common" version="3.2.0.0" targetFramework="net451" /> 
+0

Grazie. Questa risposta mostra infine quali pacchetti devono essere caricati e come configurare sia MVC che WebAPI FluentValidation senza fabbriche e classi proxy pazze. Nessuna di queste informazioni è disponibile sulla documentazione di FluentValidation, tuttavia l'autore mantiene tutti questi pacchetti. – ppumkin

0

ho speso un sacco di tempo cercando di trovare un buon modo per aggirare il fatto che WebAPI ModelValidatorProvider memorizza i validatori come single. Non volevo taggare le cose con i filtri di convalida, quindi ho finito per iniettare IKernel nel validatore e usarlo per ottenere il contesto.

public class RequestValidator : AbstractValidator<RequestViewModel>{ 
    public readonly IDbContext context; 

    public RequestValidator(IKernel kernel) { 
     this.context = kernel.Get<IDbContext>(); 

     RuleFor(r => r.Data).SetValidator(new DataMustHaveValidPartner(kernel)).When(r => r.RequestableKey == "join"); 
    } 
} 

Questo sembra funzionare anche se il validatore è memorizzato come un singleton. Se anche voi volete essere in grado di chiamare con il contesto, si può solo creare un secondo costruttore che accetta IDbContext e far passare il IKernel costruttore IDbContext utilizzando kernel.Get<IDbContext>()

0

Questo certamente non è raccomandato in quanto la classe è interno, ma è possibile rimuovere i servizi di IModelValidatorCache nella configurazione di WebApi.

public static class WebApiConfig 
{ 
    public static void Register(HttpConfiguration config) 
    { 
     config.Services.Clear(Type.GetType("System.Web.Http.Validation.IModelValidatorCache, System.Web.Http")); 
    } 
} 
Problemi correlati