Server-side validation with FluentValidation and AJAX

FluentValidation is an awesome library for doing model validation and its MVC component provides easy integration with jQuery.validate as well the unobtrusive counterpart. However, the client side capability of FluentValidation is presently limited to support for only a couple of validation rule types (there’s a small bullet list at the bottom of the first section).

So… What are we supposed to do for more complex validation rules that are going to have to round trip to the server?

This is where AJAX comes to the rescue! It can help you make your form posts appear a little more seamless to the client side and it allows you to provide JavaScript based actions on your form posts instead. In my sample site, I used MVC unobtrusive AJAX forms, but the same solution should work for posts made directly using $.ajax.

Sample site with validation error via AJAX
Use AJAX requests to display validation rules evaluated on the server

To make it all work, I’m going to assume you’ve already plugged in FluentValidation and got your model validators wired up. Then we’re going to:

  1. Add a custom action filter attribute to our controller action to intercept AJAX requests and dump model state errors to a JSON response without executing our action.
  2. Define a custom jQuery.Validate rule/method for flagging server errors.
  3. Add a jQuery AJAX prefilter to detect whether a JSON response looks like the JSON response that indicates validation errors. We’ll do this by extending the jqXHR deferred promise to set a property to allow other AJAX response handlers to detect validation errors.
  4. Add a jQuery AJAX error handler to handle validation errors by appending our custom jQuery.Validate “serverError” rule to the related form field’s input validation rules with the message returned from the server.

So all that is going to look a little like so this!

1. Custom Action Filter for your action

public class AjaxValidationAttribute : ActionFilterAttribute
{
    /// <summary>
    /// Allows specific property's validation rules to be ignored so that they can be handled manually by the controller action
    /// </summary>
    /// <remarks>
    /// Should be a comma separated list of property names.
    /// </remarks>
    public string Ignore
    {
        get;
        set;
    }
 
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        // For AJAX requests only...
        if (!filterContext.Controller.ViewData.ModelState.IsValid && filterContext.HttpContext.Request.IsAjaxRequest())
        {
            var ignore = new HashSet<string>();
            if (Ignore != null)
            {
                // Split by comma and trim out the whitespace
                var properties = Ignore.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim());
                ignore.UnionWith(properties);
            }
 
            // Convert model state errors to error objects and filter those that we're meant to ignore
            var errors = FindErrors(filterContext.Controller.ViewData.ModelState).Where(e => !ignore.Contains(e.Key));
            if (errors.Any())
            {
                // Short circuit with a JSON response
                filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
                filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
                filterContext.Result = new JsonResult
                {
                    Data = new AjaxResponse<ValidationError[]>(errors.ToArray()) { TypeName = "ValidationErrors" },
                    ContentType = null,
                    ContentEncoding = null,
                    JsonRequestBehavior = JsonRequestBehavior.DenyGet
                };
            }
        }
        base.OnActionExecuting(filterContext);
    }
 
    internal static IEnumerable<ValidationError> FindErrors(ModelStateDictionary modelState)
    {
        var result = new List<ValidationError>();
        var erroneousFields = modelState.Where(ms => ms.Value.Errors.Any()).Select(x => new { x.Key, x.Value.Errors });
        foreach (var erroneousField in erroneousFields)
        {
            var fieldKey = erroneousField.Key;
            var fieldErrors = erroneousField.Errors.Select(error => new ValidationError(fieldKey, error.ErrorMessage));
            result.AddRange(fieldErrors);
        }
        return result;
    }
}

Where my models (AjaxResponse and ValidationError) that I’m outputting look like:

public class AjaxResponse<T>
{
    private string _typeName;
    public AjaxResponse(T value)
    {
        Value = value;
    }
 
    public string TypeName
    {
        get { return _typeName ?? typeof(T).Name; }
        set { _typeName = value; }
    }
 
    public T Value { getset; }
}
public class ValidationError
{
    public ValidationError(string key, string message)
    {
        Key = key;
        Message = message;
    }
 
    public string Key { getset; }
    public string Message { getset; }
}

And most importantly, you’ve decorated your action with the new “AjaxValidation” attribute. For example:

[HttpPostAjaxValidation]
public ActionResult SendMessage(CustomerMessage model)
{
    if (ModelState.IsValid)
    {
        // Do Stuff!
    }
    return PartialView("Message", model);
}

2. Custom jQuery Validate rule for server errors

;(function ($, $validator) {
    
    $validator = $validator || $.validator;
    if ($validator) {
        // Create a custom validator rule for our server errors that are just going to ensure that a different value is used
        $validator.addMethod("serverError"function (value, element) {
            return this.optional(element) || $(element).data("serverError-value") != value;
        }, $validator.format("{0} is not a valid value"));
    }
 
})(jQuery, jQuery.validator);

3. jQuery AJAX pre-filter for tracking validation error responses

// Add a flag for ajax errors to make global identification of them easier
$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
    // Based on: http://lithostech.com/2011/04/jquery-deferreds-and-the-jquery-promise-method/
    var jqXHRWrapper = $.Deferred(function (defer) {
        jqXHR
            .done(defer.resolve)
            .fail(function (jqXHR, status, thrownError) {
                jqXHR.isResponseValidationErrors = false;
                if (thrownError === "Bad Request" && jqXHR.responseJSON && jqXHR.responseJSON.TypeName === "ValidationErrors") {
                    jqXHR.isResponseValidationErrors = true;
                }
                defer.rejectWith(this, [jqXHR, status, thrownError])
            })
        ;
    });
 
    // Impose our wrapper on the jqXHR
    jqXHRWrapper.promise(jqXHR);
 
    // For compatibility, map done/fail onto success/error handlers.
    jqXHR.success = jqXHR.done;
    jqXHR.error = jqXHR.fail;
});

4. jQuery AJAX error handler for mapping server errors into field validation rules

// Add a global ajax error handler to deal with validation errors
$(document).ajaxError(function (event, jqXHR, settings, thrownError) {
    if (jqXHR.isResponseValidationErrors === true && jqXHR.responseJSON) {
        var errors = $(jqXHR.responseJSON.Value);
        errors.each(function () {
            var that = this;
            $('[name="' + that.Key + '"]').each(function () {
 
                // Store the field value that caused the error
                $(this).data("serverError-value", $(this).val());
 
                // Add the rule to this specific field
                $(this).rules('add', {
                    serverError: true,
                    messages: {
                        serverError: that.Message
                    }
                });
 
                // Trigger the validation for this field immediately
                var form = $(this).closest("form");
                if (form.validate) {
                    var validator = form.validate();
                    validator.element(this);
                }
            });
        })
    }
});

Example site

Based on a template MVC site, we add a form for sending messages. Most of our model validation rules for the “CustomerMessage” model are going to work client side, except for the one that requires that names start with an “A”.

Source: https://github.com/reddogaw/leftlobed.ajaxvalidation

Advertisements

2 thoughts on “Server-side validation with FluentValidation and AJAX

  1. Hi Andrew, I had a go implementing this wiht MVC 5 / JQuery 1.12 and am getting a JS error from JQuery – it is a failed to load resource 400 response from I think within JQuery – all the C# code works but when it ries to retunr the Ajx error I get the error. i am using partial views in Boostrap modal form using $Ajax. Error comes from this section: “undefined” != typeof XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.send && (XMLHttpRequest.prototype.send = e(XMLHttpRequest.prototype.send, function(t) {
    return function(e) {
    var u = this
    , r = n(e);
    return r && u.addEventListener(“loadend”, function() {
    o({
    requestID: r,
    statusCode: u.status
    })
    }),
    t.apply(u, arguments) <— error here
    }

    My Ajax post (abbreviated) is
    $('form', dialog).submit(function () {
    myApp.showPleaseWait(WheelTextEnum.processing);
    $.ajax({
    url: this.action,
    type: this.method,
    cache: false,
    data: $(this).serialize(),
    error: function (request, status, error) {}

    Any clues why?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s