Fixing FluentValidation quirks

FluentValidation is a fantastic validation library for MVC and Web API projects. I use it extensively and already blogged about it before. However, there are 2 minor nuances that are really starting to irritate me after years of use.

  1. FluentValidation does not handle cases where the model is null. The reason is because it is unable to derive a validator for a null object (understandably).

  2. If you use FluentValidation in a Web API project, you'll quickly discover that any error messages written to ModelState will have their properties prefixed with the action's parameter name. This is a precautionary measure used to differentiate properties between 2 models bound to the same action.

I finally decided to tackle these issues by completely replacing FluentValidation's Web API integration with a single ActionFilter (see below).

This ActionFilter fixes the null issue by looking at the action parameter's descriptor to determine the type of the parameter even if it's null. It will ignore the parameter if the descriptor indicates that it is optional. Otherwise, it will use the parameter's type to find the validator and return HTTP/400 Bad Request if the validator is found.

The prefix issue is resolved by not cascading validation to the second parameter in the action if the first parameter fails validation. This prevents us from having to prefix properties with the parameter name in ModelState because it will always contains errors for only one model at a time.

And finally... the code for the ActionFilter. (I know i need to start throwing stuff on my GitHub. I'll get to it.). This implementation assumes that you're using some kind of dependency injection container where all of your validators are registered. Most DI containers will have IDependencyResolver already implemented. You'll just have to pass it to the filter when you wire your API project.

public class FluentValidationActionFilter : ActionFilterAttribute  
{
    private readonly IDependencyResolver _resolver;

    public FluentValidationActionFilter(IDependencyResolver resolver)
    {
        _resolver = resolver;
    }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var actionArguments = GetTheActionArguments(actionContext);

        if (actionArguments == null)
        {
            return;
        }

        foreach (var actionArgument in actionArguments)
        {
            if (actionArgument.Value == null)
            {
                var actionArgumentDescriptor = GetActionArgumentDescriptor(actionContext, actionArgument.Key);

                if (actionArgumentDescriptor.IsOptional)
                {
                    continue;
                }

                var validator = GetValidatorForActionArgumentType(actionArgumentDescriptor.ParameterType);

                if (validator == null)
                {
                    continue;
                }

                actionContext.Response = new HttpResponseMessage(HttpStatusCode.BadRequest);

                return;
            }
            else
            {
                var validator = GetValidatorForActionArgument(actionArgument.Value);

                if (validator == null)
                {
                    continue;
                }

                var validationResult = validator.Validate(actionArgument.Value);

                if (validationResult.IsValid)
                {
                    continue;
                }

                WriteErrorsToModelState(validationResult, actionContext);

                return;
            }
        }
    }

    private static IEnumerable<KeyValuePair<string, object>> GetTheActionArguments(HttpActionContext actionContext)
    {
        return actionContext.ActionArguments
            .Select(argument => argument);
    }

    private static HttpParameterDescriptor GetActionArgumentDescriptor(HttpActionContext actionContext, string actionArgumentName)
    {
        return actionContext.ActionDescriptor
            .GetParameters()
            .SingleOrDefault(prm => prm.ParameterName == actionArgumentName);
    }

    private IValidator GetValidatorForActionArgument(object actionArgument)
    {
        var abstractValidatorType = typeof (IValidator<>);
        var validatorForType = abstractValidatorType.MakeGenericType(actionArgument.GetType());

        return _resolver.GetService(validatorForType) as IValidator;
    }

    private IValidator GetValidatorForActionArgumentType(Type actionArgument)
    {
        var abstractValidatorType = typeof(IValidator<>);
        var validatorForType = abstractValidatorType.MakeGenericType(actionArgument);

        return _resolver.GetService(validatorForType) as IValidator;
    }

    private static void WriteErrorsToModelState(ValidationResult validationResults, HttpActionContext actionContext)
    {
        foreach (var error in validationResults.Errors)
        {
            actionContext.ModelState.AddModelError(error.PropertyName, error.ErrorMessage);
        }
    }
}

Code more & stay happy! :)