I previously wrote a post called Custom Validation in ASP.NET Web API with FluentValidation, in which I showed how my group set up a validation framework over WebAPI using the FluentValidation NuGet package.

Custom Validation in ASP.NET Web API with FluentValidation
Validation is one of the key components in any web app. After all, we shouldnever trust any input to our applications, ever[http://stackoverflow.com/a/2794089/106356]. Up until now, my group has beenbuilding MVC-based web apps, in those apps we’ve been content to use built-in orcustom-built vali…

In this post, we'll go over how to consume those responses in an ASP.NET MVC app, as well as how to take the error messages from the API response and automatically add them to the MVC ModelState.

I have updated the sample project on GitHub with the code from this post, so it may be easier for you to pull that down first and then follow along. Whichever way you like to learn, let's get started!

exceptionnotfound/WebApiValidationDemo
Contribute to exceptionnotfound/WebApiValidationDemo development by creating an account on GitHub.

Goals and Dependencies

In building this system, we have two major goals in mind:

  1. All validation will be done on the Web API side, but the consuming MVC app will need to display the errors to the user.
  2. In order to make the first goal easier, we want the errors from the Web API project automatically imported into the MVC ModelState.

For this solution, we are using a couple of packages from NuGet:

  • JSON.NET (Allows simple serializing and deserializing to and from JSON).
  • RestSharp (Enables simpler calling of API clients)

Finally, we are using a pattern called POST-REDIRECT-GET in our MVC app which enables us to pass ModelStates from one action to another (check out that post for more details).

Remember the Alamo Models!

First, let's remind ourselves what the models and package looked like from the previous post.

public class User
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime BirthDate { get; set; }
    public string Username { get; set; }
}

public class UserValidator : AbstractValidator<User>
{
    public UserValidator()
    {
        RuleFor(x => x.FirstName)
            .NotEmpty()
            .WithMessage("The First Name cannot be blank.")
            .Length(0, 100)
            .WithMessage("The First Name cannot be more than 100 characters.");

        RuleFor(x => x.LastName)
            .NotEmpty()
            .WithMessage("The Last Name cannot be blank.");

        RuleFor(x => x.BirthDate)
            .LessThan(DateTime.Today)
            .WithMessage("You cannot enter a birth date in the future.");

        RuleFor(x => x.Username)
            .Length(8, 999)
            .WithMessage("The user name must be at least 8 characters long.");
    }
}

public class ResponsePackage
{
    public List<string> Errors { get; set; }
    public object Result { get; set; }
    public bool HasErrors
    {
        get
        {
            if (Errors != null)
            {
                return Errors.Any();
            }
            return false;
        }
    }
    public ResponsePackage(object result, List<string> errors)
    {
        Errors = errors;
        Result = result;
    }
}

Recall that in this system, any response at all will be wrapped by a ResponsePackage instance, so one of the things the consumer system will have to do is extract the JSON from the package and deserialize it to an object.

Creating the Clients

In order to call the API using RestSharp, we can use "client" classes that implement methods which call API functions. In our case, we decided to create a "base" client class which all other clients would inherit from. The base client needs three things:

  1. A constructor which passes in the current ModelState (so that validation errors from the API can be inserted into it).
  2. An Execute() method which executes the current request.
  3. An Execute<T>() method which executes the current request and automatically extracts the content of the ResponsePackage to the specified type.

Here's the code for the base client, aptly named ClientBase:

public class ClientBase : RestClient
{
    private ModelStateDictionary _modelstate;

    public ClientBase(ModelStateDictionary modelstate) : base(Constants.ApiUrl)
    {
        _modelstate = modelstate;
    }

    //This method executes the request and 
    //does not attempt to deserialize the response.  
    //We use this for updates, deletes, etc.
    public new void Execute(IRestRequest request)
    {
        var response = base.Execute(request);
        var parsedObj = JObject.Parse(response.Content);
        var apiResponse = JsonConvert.DeserializeObject<ResponsePackage>(parsedObj.ToString());
        if (apiResponse.HasErrors)
        {
            AddErrors(apiResponse);
        }
    }

    //This method expects the Result property of the response 
    //to be a JSON object that can be deserialized 
    //into an object of type T
    public new T Execute<T>(IRestRequest request) where T : new()
    {
        var response = base.Execute(request);
        var parsedObj = JObject.Parse(response.Content);
        var apiResponse = JsonConvert.DeserializeObject<ResponsePackage>(parsedObj.ToString());
        if (apiResponse.HasErrors)
        {
            AddErrors(apiResponse);
            return default(T);
        }
        return response.Extract<T>();
    }

    private void AddErrors(ResponsePackage response)
    {
        List<string> listMessagesAdded = new List<string>();
        for (int i = 0; i < response.Errors.Count; i++)
        {
            if (listMessagesAdded.Contains(response.Errors[i])) continue;
            _modelstate.AddModelError("error" + i.ToString(), response.Errors[i]);
            listMessagesAdded.Add(response.Errors[i]);
        }
    }
}

Take particular note of the AddErrors() method. This method is key to the whole operation: it is what takes the error messages out of the ResponsePackage and inserts them into the API.

Now that we've got the base client, let's create the UserClient class which will inherit from ClientBase. Recall from the previous post that we have two methods in the API: a method to get all the users and a method to add an additional one. We need corresponding methods in our UserClient class, like so:

public class UserClient : ClientBase
{
    public UserClient(ModelStateDictionary modelstate) : base(modelstate) { }

    public List<User> GetAll()
    {
        RestRequest request = new RestRequest("users/all");
        return Execute<List<User>>(request);
    }

    public void Add(User user)
    {
        RestRequest request = new RestRequest("users/add", Method.POST);
        request.AddJsonBody(user);
        Execute(request);
    }
}

There's still a piece missing. Exactly what does the Extract<T>() method above do? It reads the Result property of the ResponsePackage and deserializes it into an object of type T. We implement that in an extension class, like so:

public static class RestResponseExtensions
{
    private static string ResultPropertyName = "Result";

    public static T Extract<T>(this IRestResponse response) where T : new()
    {
        var parsedObj = JObject.Parse(response.Content);
        return JsonConvert.DeserializeObject<T>(parsedObj[ResultPropertyName].ToString());
    }
}

NOTE: IRestResponse is an interface implemented in RestSharp

We're on our way! Now that we've got the clients defined, let's set up the MVC app.

Chaos Control(ler)

Let's do the code first. Here's the UserController class:

[RoutePrefix("users")]
public class UserController : Controller
{
    [HttpGet]
    [Route("all")]
    [Route("")]
    [Route("~/")]
    public ActionResult Index()
    {
        UserClient client = new UserClient(ModelState);
        var users = client.GetAll();
        return View(users);
    }

    [HttpGet]
    [Route("add")]
    [ImportModelState]
    public ActionResult Add()
    {
        var user = new User();
        return View(user);
    }

    [HttpPost]
    [Route("add")]
    [ExportModelState]
    public ActionResult Add(User user)
    {
        UserClient client = new UserClient(ModelState);
        client.Add(user);
        if(!ModelState.IsValid)
        {
            return RedirectToAction("Add");
        }
        return RedirectToAction("Index");
    }
}

The [ExportModelState] and [ImportModelState] attributes are part of the POST-REDIRECT-GET pattern that I've written about before. Check out the post below for more information.

Using POST-REDIRECT-GET in ASP.NET MVC
Let’s use the POST-REDIRECT-GET (PRG) pattern to get rid of pesky warning popups in ASP.NET MVC.

For now, just remember that those attributes allow the ModelState to get passed from one action to another (otherwise it gets lost on redirects).

We also need to set up the Add view (If you want the code for the Index view, take a look at the GitHub project)

@model WebApiValidationDemo.Mvc.Lib.Models.User

@{
    ViewBag.Title = "Add a User";
}

@Html.ActionLink("Back to Index", "Index")

<h2>Add a User</h2>

@using (Html.BeginForm("Add", "User", FormMethod.Post))
{
    @Html.ValidationSummary() //This is key
    <div>
        <div>
            @Html.LabelFor(x => x.FirstName)
            @Html.TextBoxFor(x => x.FirstName)
        </div>
        <div>
            @Html.LabelFor(x => x.LastName)
            @Html.TextBoxFor(x => x.LastName)
        </div>
        <div>
            @Html.LabelFor(x => x.BirthDate)
            @Html.TextBoxFor(x => x.BirthDate, new { type = "date" })
        </div>
        <div>
            @Html.LabelFor(x => x.Username)
            @Html.TextBoxFor(x => x.Username)
        </div>
        <div>
            <input type="submit" value="Save" />
        </div>
    </div>
}

The Html.ValidationSummary() is key, since that will display the error messages found in the ModelState.

What Does THIS Button Do?

It saves the User. Sheesh, if you just wait a minute, you'll find out.

(My son asked me this question regarding a different app as I was writing this section. My response was the same.)

We will now test the app to ensure that it is receiving validation errors and displaying them on the page in the Html.ValidationSummary() control. As a reminder, here's the rules that validate the User:

  1. The FirstName cannot be blank.
  2. The FirstName cannot be more than 100 characters.
  3. The LastName cannot be blank.
  4. The BirthDate cannot be in the future (relative to the current date).
  5. The Username must be at least 8 characters long.

If we run the MVC app, we'll end up on a page that looks like this:

Now let's test adding a valid user and an invalid user.

Valid User

We can click on "Add a User" to see the Add a User page. Let's attempt to add a valid user, like so:

Clicking on save causes none of the validation rules to fire, so we end up back at the index page. Success!

Invalid User

Now let's try to add an invalid user.

Note that this user violates three of the rules:

  • The first name is blank.
  • The birth date is in the future.
  • The username is less than 8 characters long.

Attempting to save this user results in this:

Success! The error messages were successfully returned to the ModelState and shown to the user.

The Best Part

Here's the best part about this whole situation: adding new validation rules on the API causes zero changes on the consumers! We've removed any dependency the MVC app has on the validation implemented by the API.

Let's say we implement a new validation rule:

  • The Last Name cannot be less than 5 characters.

The resulting change to our UserValidator class looks like this:

public class UserValidator : AbstractValidator<User>
{
    public UserValidator()
    {
        ...
        RuleFor(x => x.LastName)
            .Length(5, 999)
            .WithMessage("The Last Name cannot be less than 5 characters");
        ...
    }
}

Now, let's resubmit that invalid user we just created, and here's the error messages we get:

Summary

We have now built an ASP.NET MVC app that can successfully:

  • Consume the responses sent by the Web API's custom validation layer, even though they're all wrapped in ResponsePackage.
  • Display the error message returned by the API.

Take a look at the GitHub project if you haven't already done so, and feel free to point out what I did wrong (or right, hey I can hope) in the comments.

Happy Coding!