DotNetCurry Logo

Content Validation in ASP.NET Web API - Back to Basics

Posted by: Suprotim Agarwal , on 8/8/2013, in Category ASP.NET
Views: 84185
Abstract: Today we will validate data being passed into our ASP.NET Web API service and review some gotchas that can catch us unawares

My colleague and DNC author Sumit was recently consulting for a project where devs were a little confused on how exactly Model Validation was working. Sumit and I thought it’s a good idea to write a quick refresher on Model Binding and Model Validation in ASP.NET Web API.

Let’s get started

 

The Sample ASP.NET Project

We’ll set up a sample ASP.NET Project using the Web API template and add a couple of entities to it.

web-api-template

The two entities we use are Account and Country

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace WebApiValidation.Models
{
  public class Account
  {
  [Key]
  public int Id { get; set; }
  [Required]
  public string Name { get; set; }
  public string Description { get; set; }
  [Range(1, 1000)]
  public int KeyCode { get; set; }
  [Required]
  public int CountryId { get; set; }
  [ForeignKey("CountryId")]
  public string Country { get; set; }
}
}

We build the application and then scaffold the Accounts Web API Controller.

scaffold-accounts-controller

Once the controller has been scaffolded, we can add the excellent Test client by Yao Huang from the ASP.NET Team.

web-api-test-client

This client helps us test Web API calls without having to build full-fledged client apps. To hook up the test client, open the Api.cshtml in Areas\HelpPage\Views\Help folder of the solution and add the highlighted lines below to the markup.

@Html.DisplayForModel("TestClientDialogs")
@section Scripts {
    @Html.DisplayForModel("TestClientReferences")
    <link type="text/css" href="~/Areas/HelpPage/HelpPage.css" rel="stylesheet" />
}

Once done, we are all set to test out validation in Web API.

Model Validation via Annotations

If you noted, we had applied various types of annotation attributes to the Account model. You can checkout a comprehensive list of Annotations in MSDN Online. We are using the Key, Range and Required annotations only.

To test out the validations, run the Application and click on the API link on the top right corner of the home page.

web-api-help-page

This will navigate you to the Api Help page. Here you’ll notice that the Accounts related API that were scaffolded, are now listed.

web-api-help-page-accounts

Click on the POST link to navigate to the details page that shows sample data and the “TEST API” button.

web-api-help-page-accounts-post

Clicking on “TEST API” brings up the POST dialog that allows us to POST a blob of JSON.

web-api-help-page-accounts-post-dialog-default

As per our annotations, Name is required, KeyCode range is 0-1000. So let us remove the Name and set the KeyCode to a negative value and then hit Send.

Before that we put a breakpoint in our AccountsController in the PostAccount method.

break-point-in-post-account

Now we go back to the Browser and post the following data:

{
  "Id": 1,
  "Description": "sample string 3",
  "KeyCode": -3,
  "IsObsolete": false
}

The execution will stop at the break point and we’ll see that ModelState.IsValue has returned false. On deeper inspection, we’ll find the following errors:

accounts-post-invalid-model-state-server

If we let the execution flow to continue, we’ll get a HTTP 400/BAD Request and the Response Body will contain the errors in a JSON array as follows (reformatted for readability).

{
"Message":"The request is invalid.",
"ModelState":{
      "account":
        ["Required property 'Name' not found in JSON. Path '', line 6, position 2."],
      "account.Name":
        ["The Name field is required."],
      "account.KeyCode":
        ["The field KeyCode must be between 1 and 1000."]
}}

accounts-post-invalid-model-state-client

Under Posting and Over Posting – Limits of Model Validation

From above we see our Validation is working fine, let’s add a proper Account by POSTing the following JSON

{
  "Id": 1,
  "Name": "Account 1",
  "Description": "Description of Account 1",
  "KeyCode": 4,
  "IsObsolete": false
}

This time we’ll get the following response, with a nice Location Header giving us the URL where it was created, as well as the Body containing the complete JSON object.

accounts-post-valid

The problem of Under-Posting

Now let’s say we want to Edit this Account, so we navigate to the PUT help Page and fire up the Test Client. Let’s post the following JSON to the Account with {id} = 1. The scenario we are trying to mimic is that User did not have any changes for Description, so they did not include Description at all (instead of including Description with the current value). In this case, the expectation is Description will be left alone.

{
  "Id": 1,
  "Name": "Updated Account 1",
  "KeyCode": 4,
  "IsObsolete": false
}


accounts-put-no-description

As soon as we hit send, we’ll get a HTTP 200 in response indicating all is well. However if we do a GET for id=1 now, we’ll see that the Description has been set to null so unfortunately the user had updated incorrect data. This scenario is referred to as Under Posting.

accounts-get-no-description

One easy way to avoid Under Posting is to manage things via View Models and client side libraries like Knockout. But this will avoid only non-malicious POST. People wanting to corrupt data will still be able to POST invalid data.

The better way to do it is opt for the ‘Required’ attribute. However this goes all the way down to the database if you are using the same entity. As a good practice, your View entities should be separate from Database Entities.

Over-Posting

In the Account Entity, let’s say that business rules imply that IsObsolete flag can only be set by Administrator. However if IsObsolete flag is sent with Get requests for everyone, and a value for it is accepted for all PUT and POST requests, then there is no stopping from Accidental or Malicious use. The only way in this case is to have ViewData specific for user types OR, do manual validation on Controller or Business logic to check if Current User can update a particular field or not.

Generalizing Validation via Custom Validation Filter Attribute

As we know we can hook in Action Filters via Filter Attributes. (Check the Lifecycle of a Web API Message article for more clarity on where Action Filters come into play.) To avoid having to check for model state in every Put/Post action method of every controller, we can generalize it by creating a custom action filter.

inherit-from-http-action-filter

 

Note that the ActionFilterAttribute type is present in ASP.NET MVC and Web API (Http namespace), in this case we should select System.Web.Http.Filters.

In the class, override the OnActionExecuting method and check for Valid ModelState. If state is not valid, send back a HTTP 400 and the use the CreateErrorResponse helper on the Request property to pass the error messages. The final class is as follows

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

Note that it HttpActionContext is defined in System.Web.Http.Controllers, HttpStatusCode is defined in System.Net and CreateErrorResponse helper method is defined in the System.Net.Http namespaces respectively.

Register the Filter in App_Start\WebApiConfig.cs as follows

config.Filters.Add(new ValidateModelFilterAttribute());

To use the Filter, decorate the PutAccount method with this attribute and comment out the check for Model State

[ValidateModelFilter()]
public HttpResponseMessage PutAccount(int id, Account account)
{
//if (!ModelState.IsValid)
//{
//    return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
//}
… //rest of the code remains same
}

Now run the App and try to PUT an invalid JSON like

{
  "Id": 1,
  "Description": "sample string 3",
  "KeyCode": 4,
  "IsObsolete": true
}

You will receive a Bad Request error

bad-request-via-filter

Conclusion

ASP.NET Web API Controllers can leverage Model State validation when Data Annotation attributes are correctly used. However, there are two cases to be always wary off, these are over-posting and under-posting.

To generalize Validation, we can use a custom action filter to check for Model State in the filter itself and decorating required action methods with the Filter Attribute, instead of repeating the Model State valid check all over.

Download the entire source code of this article (Github)

Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+
Further Reading - Articles You May Like!
Author
Suprotim Agarwal, MCSD, MCAD, MCDBA, MCSE, is the founder of DotNetCurry, DNC Magazine for Developers, SQLServerCurry and DevCurry. He has also authored a couple of books 51 Recipes using jQuery with ASP.NET Controls and a new one recently at The Absolutely Awesome jQuery CookBook.

Suprotim has received the prestigious Microsoft MVP award for nine times in a row now. In a professional capacity, he is the CEO of A2Z Knowledge Visuals Pvt Ltd, a digital group that represents premium web sites and digital publications comprising of Professional web, windows, mobile and cloud developers, technical managers, and architects.

Get in touch with him on Twitter @suprotimagarwal, LinkedIn or befriend him on Facebook



Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!