In MVC, I can create a Model Validator which can take Dependencies. I normally use FluentValidation for this. This allows me to, for example, check on account registration that an e-mail address hasn't been used (NB: This is a simplified example!):
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;
}
}
No such integration exists for Web API with FluentValidation. There have been a couple of attempts at this, but neither have tackled the Dependency Injection aspect and only work with static validators.
The reason this is difficult is due to the different in implementation of ModelValidatorProvider and ModelValidator between MVC and Web API. In MVC, these are instantiated per-request (hence injecting a context is easy). In Web API, they are static, and the ModelValidatorProvider maintains a cache of ModelValidators per type, to avoid unnecessary reflection lookups on every request.
I've been trying to add the necessary integration myself, but have been stuck trying to obtain the Dependency Scope. Instead, I thought I'd step back and ask if there any other solutions to the problem - if anyone has come up with a solution to performing Model Validation where dependencies can be injected.
I do NOT want to perform the validation within the Controller (I am using a ValidationActionFilter to keep this separate), which means I can't get any help from the constructor injection of the controller.
I was able to register and then access the Web API dependency resolver from the request using the GetDependencyScope() extension method. This allows access to the model validator when the validation filter is executing.
Please feel free to clarify if this doesn't solve your dependency injection issues.
Web API Configuration (using Unity as the IoC container):
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 }
);
}
Validation action filter:
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);
}
}
Model 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 Controller Action Method:
[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 (Example):
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>();
}
I've finally got this to work, but it's a bit of a bodge. As mentioned earlier, the ModelValidatorProvider will keep Singleton instances of all Validators around, so this was completely unsuitable. Instead, I'm using a Filter to run my own validation, as suggested by Oppositional. This filter has access to the IDependencyScope
and can instantiate validators neatly.
Within the Filter, I go through the ActionArguments
, and pass them through validation. The validation code was copied out of the Web API runtime source for DefaultBodyModelValidator
, modified to look for the Validator within the DependencyScope
.
Finally, to make this work with the ValidationActionFilter
, you need to ensure that your filters are executed in a specific order.
I've packaged my solution up on github, with a version available on nuget.
I have DI working with Fluent Validators in WebApi no problems. I've found that the validators get called a lot, and these sort of heavy logic validations have no place in a model validator. Model validators, in my opinion, are meant to be lightweight checking the shape of the data. Does Email
look like an email and has the caller provided FirstName
, LastName
and either Mobile
or HomePhone
?
Logic validation like Can this email be registered belongs in the service layer, not at a controller. My implementations also don't share an implicit data context since I think that's an anti-pattern.
I think the current NuGet package for this has an MVC3 dependency, so I ended up just looking at the source directly and creating my own NinjectFluentValidatorFactory
.
In App_Start/NinjectWebCommon.cs
we have the following.
/// <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;
}
I believe the only other required packages for the above are:
<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" />
I spent a lot of time trying to find a good way around the fact that WebApi ModelValidatorProvider stores the validators as singletons. I didn't want to have to tag things with validation filters, so I ended up injecting IKernel in the validator and using that to get the context.
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");
}
}
This seems to work even though the validator is stored as a singleton. If you also want to be able to call it with the context, you could just create a second constructor that takes IDbContext
and make the IKernel
constructor pass IDbContext
using kernel.Get<IDbContext>()
This certainly isn't recommended as the class is internal, but you can remove the IModelValidatorCache services in your WebApi config.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Services.Clear(Type.GetType("System.Web.Http.Validation.IModelValidatorCache, System.Web.Http"));
}
}
FluentValidation has had support for WebApi for quite sometime (not sure if your question dates before that): https://fluentvalidation.codeplex.com/discussions/533373
Quoting from the 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!
I have been using it in WebApi2 project without any issues.
来源:https://stackoverflow.com/questions/15092809/dependency-injected-validation-in-web-api