30

I'm trying to hook Fluent Validation to my MVC WEB Api project, and it doesn't wanna work.

When I use MyController : Controller -> works fine (ModelState.IsValid returns False)

but when I use MyController :ApiController ... nothing.

Does anyone have experience on how to hook those up ?

abatishchev
  • 98,240
  • 88
  • 296
  • 433
Marty
  • 3,485
  • 8
  • 38
  • 69
  • 1
    Actually there is a better web api integration in implementation: https://fluentvalidation.codeplex.com/SourceControl/network/forks/paulduran/webapisupportv2/contribution/3940 not merged however too. – abatishchev Aug 31 '13 at 10:34
  • 1
    you should accept my answer since there's been a development in FV – Dmitry Efimenko Feb 12 '14 at 15:59

6 Answers6

19

latest version of Fluent Validation (5.0.0.1) supports web api

Just install it from Nuget and register it in Global.asax like so:

using FluentValidation.Mvc.WebApi;

public class WebApiApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        ...
        FluentValidationModelValidatorProvider.Configure();
    }
}
Dmitry Efimenko
  • 10,973
  • 7
  • 62
  • 79
  • I submitted suggestion to extract Web API implementation from MVC 5 because they are totally unrelated. Will see what the author reply. – abatishchev Feb 27 '14 at 04:03
14

The answer is in this pull request.

Basically You need to implement custom ModelValidation Provider.

And a couple more things to note:

  1. Web API don't work with modelValidator from System.Web.Mvc namespace, only with the ones from System.Web.Http as noted here:

    Server side validation with custom DataAnnotationsModelValidatorProvider

  2. You don't add it like this:

    ModelValidatorProviders.Providers.Add(new WebApiFluentValidationModelValidatorProvider());`
    

    BUT like this:

    GlobalConfiguration.Configuration.Services.Add(typeof(System.Web.Http.Validation.ModelValidatorProvider), new WebApiFluentValidationModelValidatorProvider());`
    
Community
  • 1
  • 1
Marty
  • 3,485
  • 8
  • 38
  • 69
  • One more thing to Note: there's no way how to invoke "Revalidate" or "TryValidate" in WebApi controller. So consider if You need this hooking up in the first place. – Marty Oct 24 '12 at 08:52
  • 1
    Please mote that there is a better web api integration implementation: https://fluentvalidation.codeplex.com/SourceControl/network/forks/paulduran/webapisupportv2/contribution/3940 not merged however too. – abatishchev Aug 31 '13 at 10:37
  • 3
    Just a remark. If you are using a DI container like Ninject, the above won't work. You must instead add a binding in yout DI config. Would be something like the following: `kernel.Bind().To();` `kernel.Bind().To().InSingletonScope();` – cleftheris Dec 18 '13 at 13:55
  • would you be able to post an example of `WebApiFluentValidationModelValidatorProvider`? – Dmitry Efimenko Jan 25 '14 at 09:16
  • 1
    @Dmitry I think it's meant to be FluentValidation.WebApi.FluentValidationModelValidatorProvider – Mathew Jul 20 '16 at 08:19
4

I have found another simple solution for using FluentValidation in Web API, but it lacks integration with ModelState and Metadata. However, when building an API that doesn't need to return the entire ModelState to the client (as is needed in MVC to rebuild the page), I have found the trade-off for simplicity to be worthwhile. Whenever an API input is invalid, I return a 400 Bad Request status code with a list of property IDs and error messages. To do this, I use a simple ActionFilterAttribute:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class ValidateInputsAttribute : ActionFilterAttribute
{
    private static readonly IValidatorFactory ValidatorFactory = new AttributedValidatorFactory();

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        base.OnActionExecuting(actionContext);
        var errors = new Dictionary<string, string>();
        foreach (KeyValuePair<string, object> arg in actionContext.ActionArguments.Where(a => a.Value != null))
        {
            var argType = arg.Value.GetType();
            IValidator validator = ValidatorFactory.GetValidator(argType);
            if (validator != null)
            {
                var validationResult = validator.Validate(arg.Value);
                foreach (ValidationFailure error in validationResult.Errors)
                {
                    errors[error.PropertyName] = error.ErrorMessage;
                }
            }
        }
        if (errors.Any())
        {
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, errors);
        }
    }
}

This attribute can be added as a global filter, to individual controllers/actions, or to a base class.

This code can certainly be improved, but it has served me well so far so I wanted to make it available to others. Here are some of its shortcomings:

  1. Null inputs are not validated. I thought that this would be more of a problem, but in practice it simply doesn't happen much (if at all) in our app. My controllers throw ArgumentNullExceptions for null inputs which would return a 500 to the client informing the client that the input cannot be null.
  2. I can't use ModelState in my controllers. But, after validating the required inputs are non-null, I already know that the ModelState is valid so this may actually serve to simplify code. But it's important for devs to know not to use it.
  3. Right now this implementation is hard coded for the AttributedValidatorFactory. This should be abstracted, but it's been pretty low on my priority list so far.
Matt Scully
  • 403
  • 4
  • 9
  • 1
    Your implementation can be split/abstracted into few parts actually: looking-up for a validator (DI container based factory), validation itself and processing validation result (setting error to context). – abatishchev Aug 31 '13 at 10:29
3

As I was looking to solve this I wanted to make it so that the same validator instance could be used for MVC and Web API. I was able to accomplish this by making two factories and using them together.

MVC Factory:

public class MVCValidationFactory : ValidatorFactoryBase
{
    private readonly IKernel _kernel;

    public MVCValidationFactory(IKernel kernel)
    {
        _kernel = kernel;
    }

    public override IValidator CreateInstance(Type validatorType)
    {
        var returnType = _kernel.TryGet(validatorType);

        return returnType as IValidator;
    }
}

API Factory:

public class WebAPIValidationFactory : ModelValidatorProvider
{
    private readonly MVCValidationFactory _mvcValidationFactory;

    private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

    public WebAPIValidationFactory(MVCValidationFactory mvcValidationFactory)
    {
        _mvcValidationFactory = mvcValidationFactory;
    }

    public override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, IEnumerable<ModelValidatorProvider> validatorProviders)
    {
        try
        {
            var type = GetType(metadata);

            if (type != null)
            {
                var fluentValidator =
                    _mvcValidationFactory.CreateInstance(typeof(FluentValidation.IValidator<>).MakeGenericType(type));

                if (fluentValidator != null)
                {
                    yield return new FluentValidationModelValidator(validatorProviders, fluentValidator);
                }
            }
        }
        catch (Exception ex)
        {
            Log.Error(ex);
        }

        return new List<ModelValidator>();
    }

    private static Type GetType(ModelMetadata metadata)
    {
        return metadata.ContainerType != null ? metadata.ContainerType.UnderlyingSystemType : null;
    }

The trick then was figuring out how to run the validation for both MVC and Web API. I ended up creating a wrapper for the IValidator<> that worked with the ModelValidator signature.

public class FluentValidationModelValidator : ModelValidator
{
    public IValidator innerValidator { get; private set; }

    public FluentValidationModelValidator(
        IEnumerable<ModelValidatorProvider> validatorProviders, IValidator validator)
        : base(validatorProviders)
    {
        innerValidator = validator;
    }

    public override IEnumerable<ModelValidationResult> Validate(ModelMetadata metadata, object container)
    {
        if (InnerValidator != null && container != null)
        {
            var result = innerValidator.Validate(container);

            return GetResults(result);
        }

        return new List<ModelValidationResult>();
    }

    private static IEnumerable<ModelValidationResult> GetResults(FluentValidation.Results.ValidationResult result)
    {
        return result.Errors.Select(error =>
            new ModelValidationResult
            {
                MemberName = error.PropertyName,
                Message = error.ErrorMessage
            }));
    }
}

The last part was to wire up the validators in the Global.asax:

MVCValidationFactory mvcValidationFactory = new MVCValidationFactory(KernelProvider.Instance.GetKernel());

GlobalConfiguration.Configuration.Services.Add(
    typeof(ModelValidatorProvider),
    new WebAPIValidationFactory(mvcValidationFactory));

ModelValidatorProviders.Providers.Add(new FluentValidationModelValidatorProvider(mvcValidationFactory));

DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;

Sorry this was a bit long, but hopefully it helps someone out.

abatishchev
  • 98,240
  • 88
  • 296
  • 433
Joel
  • 56
  • 2
  • Are You able to invoke "Validate" from the controller ? The problem for me, was I needed the validation to be executed in the middle of the "MVC controller method" (e.x. POST). So hooking it up to the MVC Factory didn't work for the API, cause it would reject the object being posted without entering the method (while in my case, the method would assign some of the value needed to be valid) – Marty Feb 23 '13 at 20:09
  • Your answer is super awesome I must say. – abatishchev Feb 27 '14 at 04:01
  • 1
    This turned out to be extremely over complicated and had some really strange behaviour. You can use the standard Ninject factory (in your example the MVCFactory) by passing the factory into these `FluentValidation.Mvc.FluentValidationModelValidatorProvider.Configure(` and `FluentValidation.WebApi.FluentValidationModelValidatorProvider.Configure(` - Then also the Filter just overrides the System.Net.Http onActionExecuting and that works for both MVC and WebAPI - Which is good because MVC6 only uses System.Net and the MVC namepspace has been almost erradicated – Piotr Kula Jun 07 '17 at 15:11
2

In the WebApiConfig add two lines

public static class WebApiConfig
{
   public static void Register(HttpConfiguration config)
   {
       // snip...
       //Fluent Validation
       config.Filters.Add(new ValidateModelStateFilter());
       FluentValidationModelValidatorProvider.Configure(config);
   }
}

Create a model and a validator as follows -

[Validator(typeof(PersonCreateRequestModelValidator))] 
public class PersonCreateRequestModel
{
    public Guid PersonId { get; set; }
    public string Firstname { get; set; }
    public string Lastname { get; set; }
}


public class PersonCreateRequestModelValidator : AbstractValidator
{
    //Simple validator that checks for values in Firstname and Lastname
    public PersonCreateRequestModelValidator()
    {
        RuleFor(r => r.Firstname).NotEmpty();
        RuleFor(r => r.Lastname).NotEmpty();
    }
}

That's about all you need. Just write the controller as you would normally.

public IHttpActionResult Post([FromBody]PersonCreateRequestModel requestModel)
{
    //snip..
    //return Ok(some new id);
}

If you want a full source code example you can get it here - http://NoDogmaBlog.bryanhogan.net/2016/12/fluent-validation-with-web-api-2/

Bryan
  • 5,065
  • 10
  • 51
  • 68
  • I am not sure why but this just doesn't work :( Even though I have the code like you show it... and I know its supposed to work but it just doesn't. Which is frustrating. I have both MVC and WebAPI though.. so I suppose that must be an issue somewhere. – Piotr Kula Jun 07 '17 at 10:27
  • 1
    @ppumkin the blog post has full source code. Download and try that. – Bryan Jun 07 '17 at 15:04
  • 1
    I ended up using something else `FluentValidation.Mvc.FluentValidationModelValidatorProvider.Configure` and then `FluentValidation.WebApi.FluentValidationModelValidatorProvider.Configure` without any complicated factories and proxies, and just one FilterAttribute. Not sure why it was so difficult for me to find that, but its working for me not in MVC and WebAPI with 3ish lines of code :D – Piotr Kula Jun 07 '17 at 15:07
-4

Latest version of Fluent Validation doesn't support Mvc 4 or Web Api. Read this.

jks
  • 154
  • 1
  • 9