actionfilterattribute

Custom Validation in ASP.NET Web API with FluentValidation

Validation is one of the key components in any web app. After all, we should never trust any input to our applications, ever. Up until now, my group has been building MVC-based web apps, in those apps we've been content to use built-in or custom-built validation solutions for any validation-type procedures we needed to write.

Now, though, we've transferred over to building a web service written in ASP.NET Web API, and the old validation structure we had in place just doesn't work very well in that new environment. Consequently we had to hammer out a new validation structure, and I'm particularly proud of how we set that up, so I wanted to share it here. Let me know if it helps you as well. First, though, a little background on how our app was set up.

Architecture of the App

Our app has the following layers:

Clients (ASP.NET MVC, iPhone apps, etc)  
|
API (Web API)  
|
Database  

It's a pretty standard service-based application, at least from the 10,000-foot view.

The problem is that we want our validation to be performed in the API layer, and not make the clients responsible for implementing their own validation. In other words, we need to do the following:

  1. Validate all incoming requests according to a set of rules.
  2. Return any validation error messages to the calling clients.

We can tackle Problem 1 by using one of my favorite NuGet packages, FluentValidation.

Setting Up FluentValidation

Let's add FluentValidation's WebAPI package to our app, like so:

Now we can set up a class that we will need to validate. Here's a sample User class.

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

In order to set up FluentValidation, we need to create a Validator class which inherits from AbstractValidator, and add a Validate attribute to the User class. Finally, we need to define the validation rules that apply to the properties of User. The final result looks like this:

[Validator(typeof(UserValidator))]
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.");
    }
}

I'm not going to get into how to set up these rules; see my earlier tutorial for how to do this.

Only thing left to do to enable FluentValidation is to turn on the ModelValidatorProvider in WebApiConfig.cs, like so:

public static class WebApiConfig  
{
    public static void Register(HttpConfiguration config)
    {
        ...
        FluentValidationModelValidatorProvider.Configure(config);
    }
}

Validating the Request

Now that we have the validation rules in place, we need a way to validate that all requests submitted to this service are valid. We can do this by using an ActionFilterAttribute. Remember that FluentValidation places validation errors into the ModelState so our ActionFilterAttribute will need to check the ModelState's status.

The filter looks like this:

public class ValidateModelStateFilter : ActionFilterAttribute  
{
    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        if (!actionContext.ModelState.IsValid)
        {
            actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.BadRequest, actionContext.ModelState);
        }
    }
}

What this filter does is return a new error response if the submitted Request has validation errors. We know that ModelState.IsValid will represent whether or not the Request has validation errors because FluentValidation automatically inserts its errors into the ModelState.

Now we just need to add this filter to the Web API Pipeline in WebApiConfig.cs, like so:

public static class WebApiConfig  
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services
        config.Filters.Add(new ValidateModelStateFilter());

        // Web API routes
        ...

        FluentValidationModelValidatorProvider.Configure(config);
    }
}

Wrapping The Response

Now we come to a fundamental problem with this setup: the type of the response could be anything, yet we need to append existing validation errors to the response. To solve this, we created a simple ResponsePackage class:

public class ResponsePackage  
{
    public List<string> Errors { get; set; }

    public object Result { get; set; }
    public ResponsePackage(object result, List<string> errors)
    {
        Errors = errors;
        Result = result;
    }
}

The idea is that every response from the API will be wrapped by an instance of this class, which contains any errors that occurred.

We now need something that will check every response from the API and wrap it in ResponsePackage. To do this, we use a DelegatingHandler:

public class ResponseWrappingHandler : DelegatingHandler  
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        //Step 1: Wait for the Response
        var response = await base.SendAsync(request, cancellationToken);

        return BuildApiResponse(request, response);
    }

    private HttpResponseMessage BuildApiResponse(HttpRequestMessage request, HttpResponseMessage response)
    {
        object content;
        List<string> modelStateErrors = new List<string>();

        //Step 2: Get the Response Content
        if (response.TryGetContentValue(out content) && !response.IsSuccessStatusCode)
        {
            HttpError error = content as HttpError;
            if (error != null)
            {
                //Step 3: If content is an error, return nothing for the Result.
                content = null; //We have errors, so don't return any content
                //Step 4: Insert the ModelState errors              
                if (error.ModelState != null)
                {
                    //Read as string
                    var httpErrorObject = response.Content.ReadAsStringAsync().Result;

                    //Convert to anonymous object
                    var anonymousErrorObject = new { message = "", ModelState = new Dictionary<string, string[]>() };

                    // Deserialize anonymous object
                    var deserializedErrorObject = JsonConvert.DeserializeAnonymousType(httpErrorObject, anonymousErrorObject);

                    // Get error messages from ModelState object
                    var modelStateValues = deserializedErrorObject.ModelState.Select(kvp => string.Join(". ", kvp.Value));

                    for (int i = 0; i < modelStateValues.Count(); i++)
                    {
                        modelStateErrors.Add(modelStateValues.ElementAt(i));
                    }
                }
            }
        }

        //Step 5: Create a new response
        var newResponse = request.CreateResponse(response.StatusCode, new ResponsePackage(content, modelStateErrors));

        //Step 6: Add Back the Response Headers
        foreach (var header in response.Headers) //Add back the response headers
        {
            newResponse.Headers.Add(header.Key, header.Value);
        }

        return newResponse;
    }
}

Let's walk through what this does.

  • Step 1: We wait for the response. After all, for this handler, we don't care about the request or anything besides the response.
  • Step 2: We get the response content. What that content is determines whether or not we need to run Steps 3-5.
  • Step 3: If the content is an error (of type HttpError), set the returned content to null.
  • Step 4: Insert the ModelState errors. We do this by reading the response content, deserializing that first to an anonymous object then to a JObject (using Json.Net) and finally extracting the error messages themselves.
  • Step 5: Create a new response.
  • Step 6: Add back the response headers (they should be the same in the new response as in the generated response).

In short, this wrapper will wrap all responses in an instance of ResponsePackage, and if there are any errors in the ModelState, those errors get included in the ResponsePackage.

The final step, of course, is to place the handler in the Web API Pipeline. We're done with all the steps, so here's the complete WebApiConfig.cs file:

public static class WebApiConfig  
{
    public static void Register(HttpConfiguration config)
    {
        // Web API configuration and services

        config.Filters.Add(new ValidateModelStateFilter());
        config.MessageHandlers.Add(new ResponseWrappingHandler());

        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        FluentValidationModelValidatorProvider.Configure(config);
    }
}

Testing the Validation

Now we can test that this works using Postman. First, we'll need some controller actions. Here's a GET for retrieving users and a POST for submitting a new one:

[RoutePrefix("users")]
public class HomeController : ApiController  
{
    [HttpGet]
    [Route("all")]
    [ValidateModelStateFilter]
    public IHttpActionResult GetAll()
    {
        var users = new List<User>();
        users.Add(new User()
        {
            FirstName = "Mark",
            LastName = "Watney",
            BirthDate = new DateTime(2003, 8, 7),
            Username = "mwatney549"
        });

        users.Add(new Models.User()
        {
            FirstName = "Melissa",
            LastName = "Lewis",
            BirthDate = new DateTime(1999, 1, 26),
            Username = "cmdrlewis"
        });

        users.Add(new Models.User()
        {
            FirstName = "Rick",
            LastName = "Martinez",
            BirthDate = new DateTime(1998, 4, 5),
            Username = "mmartinez"
        });

        return Ok(users);
    }

    [HttpPost]
    [Route("add")]
    public IHttpActionResult Add(User user)
    {
        return Ok();
    }
}

Note that, because we've already included the ActionFilter and the DelegatingHandler in the pipeline, the Controller doesn't explicitly call them anywhere.

Let's have Postman submit the GET request. Here's what that looks like:

As you can see, we get back the users as a collection under the property Result, with no error messages.

Now let's submit a valid request to add a user. Here's the JSON we'll be submitting:

{
    "FirstName": "Beth",
    "LastName": "Johanssen",
    "BirthDate": "2004-5-5",
    "Username": "bjohanssen"
}

And here's the response we get from Postman:

Postman is showing no errors, with status code 200 OK and no result (remember, the controller action didn't do anything except return Ok();).

Now let's submit invalid JSON, like this:

{
    "FirstName": "Beth",
    "LastName": "",
    "BirthDate": "2004-5-5",
    "Username": "bjoha"
}

This JSON object violates two of our custom validation rules:

  • The last name must not be blank AND
  • The username must be at least 8 characters long.

Here's the Postman response for that JSON:

This is, of course, what we expect. We violated two validation rules, and therefore got back two errors. Success!

Summary

This architecture allows us to do the validation on the Web API side of the app, and automatically include all validation errors in the response. We need two NuGet packages and three major pieces to implement this solution. The NuGet packages are FluentValidation.WebApi and Newtonsoft.Json, and the other pieces are as follows:

  • A ResponsePackage class which will have properties for both the result and the errors collection.
  • An ActionFilterAttribute that validates the ModelState and returns an error if it is invalid.
  • A DelegatingHandler which wraps all responses from the API into the ResponsePackage class.

I've got a sample project over on GitHub, go check it out, and let me know if it helps you with your projects in the comments.

Happy Coding!

What is the ActionFilterAttribute? - ASP.NET MVC Demystified

One of the ways MVC allows us to fine-tune what an action does is via the use of Action Filter attributes. Action Filters are attributes which inherit from the ActionFilterAttribute class, and can execute either before or after a decorated action (or before/after every action in a controller) and modify how the action is handled. In this post we'll explore creating a custom Action Filter by inheriting from ActionFilterAttribute.

What is an ActionFilter?

In ASP.NET, an "Action Filter" in the loose sense applies to any action attribute. These attributes can implement one or more of the following interfaces:

Note that if a given class implements more than one of those interfaces, the corresponding methods from the interfaces will be executed in the order listed above.

In order to make creating these attributes easier, .NET exposes the ActionFilterAttribute class which implements IActionFilter and IResultFilter. This class exposes four methods that you can override:

  • OnActionExecuting: Fires before the action is executed.
  • OnActionExecuted: Fires after the action is executed.
  • OnResultExecuting: Fires before the action result is executed.
  • OnResultExecuted: Fires after the action result is executed.

Each of these methods have different use cases. For example, if you wanted to modify the result of the action based on some additional data, the best place to do that would be OnResultExecuting.

You've actually already seen ActionFilter attributes in action if you've ever seen HandleErrorAttribute or AuthorizeAttribute, as both of those classes derive from the base ActionFilterAttribute class.

Let's build a simple, custom Action Filter to show these ideas in action.

Setting Up the Demo App

We'll be using a database schema that looks like this:

Each user can create one or more Reports in this schema. What we are going to build is an Action Filter Attribute that restricts access to these reports to only the User that created them. In order to show how this works more transparently, we are also going to allow the current user of the app to "impersonate" the database-stored users by placing a User ID into Session, then using that ID to see whether or not that user can access a given Report.

Confused? It's not too bad. Let's get started!

Selecting the Current User

Let's start with the Home/Index view model, controller actions, and view; these allow us to select the current user and store it in Session.

ViewModels/Home/HomeIndexVM.cs
public class HomeIndexVM  
{
    public List<User> Users { get; set; }

    [DisplayName("Select a User:")]
    public int SelectedUser { get; set; }
}
Controllers/HomeController.cs
public class HomeController : Controller  
{
    [HttpGet]
    public ActionResult Index()
    {
        HomeIndexVM model = new HomeIndexVM();
        model.Users = UserManager.GetAll();
        return View(model);
    }

    [HttpPost]
    public ActionResult Index(HomeIndexVM model)
    {
        Session["CurrentUserID"] = model.SelectedUser;
        TempData["FlashMessage"] = "You are now " + UserManager.GetByID(model.SelectedUser).Name + ".";
        return RedirectToAction("Index");
    }
}
Views/Home/Index.cshtml (Simplified)
@model ActionFilterDemo.ViewModels.Home.HomeIndexVM

<h2>ActionFilter Demo</h2>

@if (TempData["FlashMessage"] != null)
{
    <span><strong>FLASH:</strong>@TempData["FlashMessage"].ToString()</span>
}

@using(Html.BeginForm())
{
    <div>
        <div>
            @Html.LabelFor(x => x.SelectedUser)
            @Html.DropDownListFor(x=>x.SelectedUser, new SelectList(Model.Users, "UserID", "Name"))
        </div>
        <div>
            <input type="submit" value="Go" />
        </div>
    </div>
}

Listing and Viewing the Reports

The next part of the app is pretty simple: listing and viewing the reports.

ViewModels/Reports/ReportsIndexVM.cs
public class ReportsIndexVM  
{
    public List<Report> Reports { get; set; }
}
Controllers/ReportsController.cs
public class ReportsController : Controller  
{
    [HttpGet]
    public ActionResult Index()
    {
        ReportsIndexVM model = new ReportsIndexVM();
        model.Reports = ReportsManager.GetAll();
        return View(model);
    }

    [HttpGet]
    public ActionResult View(int id)
    {
        var report = ReportsManager.GetByID(id);
        return View(report);
    }
}
Views/Reports/Index.cshtml
@model ActionFilterDemo.ViewModels.Reports.ReportsIndexVM

@{
    ViewBag.Title = "Reports";
}

<h2>Reports</h2>

<table>  
    <thead>
        <tr>
            <th>
                Report Name
            </th>
            <th>
                Created by
            </th>
        </tr>
    </thead>
    <tbody>
        @foreach(var report in Model.Reports)
        {
            <tr>
                <td>
                    @Html.ActionLink(report.Title, "View", new { id = report.ReportID })
                </td>
                <td>
                    @report.User.Name
                </td>
            </tr>
        }
    </tbody>
</table>  
Views/Reports/View.cshtml
@model ActionFilterDemo.DataAccess.Model.Report

@{
    ViewBag.Title = "View";
}

<h3>Report: @Model.Title</h3>  
<span>Created By: @Model.User.Name</span>  

Now we're ready to start building the Action Filter.

Building the Action Filter

We want our filter to check the Session["CurrentUserID"] value against the Report ID, and see if the current user created the specified report. If s/he did, we proceed as normal, and if not, we redirect back to Home/Index.

First, we need to create a class that inherits from ActionFilterAttribute:

Attributes/CanEditReport.cs
public class CanEditReport : ActionFilterAttribute  
{
}

What we want to do in this scenario is evaluate the CurrentUserID BEFORE the action executes, so we override OnActionExecuting:

public class CanEditReport : ActionFilterAttribute  
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
    }
}

Notice the ActionExecutingContext parameter. That parameter is extremely important as it contains the information (such as route values, query string parameters, and controller data) that we need to access.

Now, for the first step, let's get the report ID from the route parameter "id":

public class CanEditReport : ActionFilterAttribute  
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var reportID = Convert.ToInt32(filterContext.ActionParameters["id"]);
        var report = ReportsManager.GetByID(reportID);
    }
}

The ActionParameters collection contains all of the parameters being passed to the action that this attribute decorates, so it occurs after the model binding has completed.

Next, let's check the current user in Session:

public class CanEditReport : ActionFilterAttribute  
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var reportID = Convert.ToInt32(filterContext.ActionParameters["id"]);
        var report = ReportsManager.GetByID(reportID);
        int userID = 0;
        bool hasID = int.TryParse(filterContext.HttpContext.Session["CurrentUserID"].ToString(), out userID);
        if (!hasID)
        {
            //What goes here?
        }
    }
}

Notice that we can access the Session through the HttpContext property of ActionExecutingContext.

Now the question becomes this: what goes inside that if clause? If they don't have a valid ID, we want to redirect back to Home/Index with a flash message, but we cannot call RedirectToAction from an Action Filter. Instead, we set the Result of the ActionExecutingContext to a new RedirectToRouteResult, like so:

public class CanEditReport : ActionFilterAttribute  
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var reportID = Convert.ToInt32(filterContext.ActionParameters["id"]);
        var report = ReportsManager.GetByID(reportID);
        int userID = 0;
        bool hasID = int.TryParse(filterContext.HttpContext.Session["CurrentUserID"].ToString(), out userID);
        if (!hasID)
        {
            filterContext.Controller.TempData["FlashMessage"] = "Please select a valid User to access their reports.";
                //Change the Result to point back to Home/Index
            filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Home", action = "Index" }));
        }
    }
}

There's still one more scenario we need to handle: what if the user that's trying to access this report isn't the person that created it? In this scenario, just like the invalid-user-ID one, we want to redirect to Home/Index with a flash message.

Our final CanEditReport class looks like this:

public class CanEditReport : ActionFilterAttribute  
{
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var reportID = Convert.ToInt32(filterContext.ActionParameters["id"]);
        var report = ReportsManager.GetByID(reportID);
        int userID = 0;
        bool hasID = int.TryParse(filterContext.HttpContext.Session["CurrentUserID"].ToString(), out userID);
        if (!hasID)
        {
            filterContext.Controller.TempData["FlashMessage"] = "Please select a valid User to access their reports.";
            //Change the Result to point back to Home/Index
            filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Home", action = "Index" }));
        }
        else //We have selected a valid user
        {
            if(report.UserID != userID)
            {
                filterContext.Controller.TempData["FlashMessage"] = "You cannot view Reports you have not created.";
                //Change the Result to point back to Home/Index
                filterContext.Result = new RedirectToRouteResult(new RouteValueDictionary(new { controller = "Home", action = "Index" }));
            }
        }
        base.OnActionExecuting(filterContext);
    }
}

All we have to do now is decorate the appropriate action in the controller:

Controllers/ReportsController.cs
public class ReportsController : Controller  
{
    [HttpGet]
    public ActionResult Index()
    {
        ReportsIndexVM model = new ReportsIndexVM();
        model.Reports = ReportsManager.GetAll();
        return View(model);
    }

    [HttpGet]
    [CanEditReport]
    public ActionResult View(int id)
    {
        var report = ReportsManager.GetByID(id);
        return View(report);
    }
}

Now, when we run the app, the system will look to see which user we currently are impersonating, and if we try to access a report that that user didn't create, we will get kicked back out to the Home/Index view.

As always, you can check out the sample project on GitHub, and please let me know what you think of this demo in the comments!

Happy Coding!