13

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.

Community
  • 1
  • 1
Richard
  • 29,854
  • 11
  • 77
  • 120
  • This is an excellent question as it sounds like you really did your research prior to asking. I'm not quite sure whether at the point this question was asked you were really struggling with being able to inject dependencies, or whether the issue was that due to the caching that was occurring that any dependencies injected wouldn't be resolved each time the validation is performed. This is the situation my team has run up against for which I opened an issue with FluentValidation: https://github.com/JeremySkinner/FluentValidation/issues/108. Your solution below may be a viable solution for us. – Derek Greer Aug 20 '15 at 12:58
  • I've just put them on nuget if you want to make use of them – Richard Aug 20 '15 at 14:50

6 Answers6

7

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>();
}
Oppositional
  • 11,141
  • 6
  • 50
  • 63
  • Unfortunately, the Web API validation has already been called at this stage, so it's not possible to do this with the normal validation, which is what I was hoping to do. However, it does look like it might be possible to do a second round of validation in this way, which is a bit of a bodge but better than nothing! I'll give it a go. – Richard Mar 01 '13 at 11:34
  • I am definitely interested in hearing how you solve this, as I would like to do something similar, so post your solution if you have time. – Oppositional Mar 04 '13 at 05:00
  • I wasn't able to get it working on Friday mostly because you don't have access to the model properly from within the Filter. Trying some more ideas related to this today though. – Richard Mar 04 '13 at 09:26
  • I've got everything working now, and have posted details of how as a separate answer. It required a fair bit more work to get the validation to work neatly, and I'll try and publish everything neatly at the weekend. As your answer got me on the right path, I've awarded the bounty to you. – Richard Mar 05 '13 at 22:33
  • Wow, wasn't expecting that. Thanks. I look forward to seeing the packaged solution you plan on posting. – Oppositional Mar 05 '13 at 22:36
4

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.

Richard
  • 29,854
  • 11
  • 77
  • 120
1

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" />
Piotr Kula
  • 9,597
  • 8
  • 59
  • 85
Robert Paulson
  • 17,603
  • 5
  • 34
  • 53
  • Thank you. This answer finally shows which packages we need to have loaded and how to setup both MVC and WebAPI FluentValidation without crazy factories and proxy classes. None of this information is available on the FluentValidation documentation, yet the author maintains all these packages. – Piotr Kula Jun 07 '17 at 13:38
1

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

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>()

JustMaier
  • 2,101
  • 21
  • 23
-1

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.

Mrchief
  • 75,126
  • 20
  • 142
  • 189
  • The question is not about using FluentValidation in Web API, it's about using it in conjunction with Dependency Injection, which cannot be done in the standard implementation, due to the validators being static. – Richard May 12 '14 at 18:16
  • Sorry my bad... I was reading it in a diff context. I'm not sure if my project has any validators that take deps rt now, but I'll check and come back. by the sound of it, I think I probably have to delete my answer... :| – Mrchief May 12 '14 at 18:20