APIs are everywhere these days. We access the web now on a plethora of devices, from laptops to smart watches. More often than not, they’re a key part of our architecture. What makes them so important? The data. Whether we’re building a mobile app or a thin web client, data is key. Just as important as the data our API does accept is the data it doesn’t accept. The invalid data. The required field we don’t pass across. The password we set to the wrong length. Any API in the wild needs to validate the data we pass in. In the .NET world, we capture those errors using ModelState
. Trouble is, ModelState
in MVC is a bit different from WebAPI. How can we make them talk to each other?
It's All About the ModelState
The issue stems from the fact that the binding process is different between WebAPI and MVC. WebAPI uses formatters to deserialise the request body. This results in a different structure. In WebAPI, the name of the containing class forms part of the error key. That isn’t the case in MVC. This means we need to translate the WebAPI ModelState
within our MVC application somehow. We’ll then be able to display validation messages in our web client. This article is part 3 of a series on calling WebAPI from MVC. In the first part, we discussed calling a WebAPI controller from MVC. In the second part, we examined how to post to a WebAPI controller. Let’s now look at what happens if we need to validate the data we post across.
If you want to follow along, you can grab the Visual Studio solution from the previous article. If you'd rather just download the finished code, you can get it from the link below.
View the original article
What Do We Do If Things Go Bad?
To start with, let’s add a couple of required attributes to the API model. We’ll also return a 401 Bad Request HTTP status code if the model isn’t valid.
public class ProductApiModel
{
public int ProductId { get; set; }
[Required]
public string Name { get; set; }
[Required]
public string Description { get; set; }
public DateTime CreatedOn { get; set; }
}
public class ProductsController : ApiController
{
public IHttpActionResult Post(ProductApiModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
return Ok(7);
}
}
Notice that we pass the entire ModelState
back from the API. This allows us to parse it and extract the errors to display in our MVC application. We’ll hold them in an ErrorState
property. Let’s see how that would look.
public abstract class ApiResponse
{
public bool StatusIsSuccessful { get; set; }
public ErrorStateResponse ErrorState { get; set; }
public HttpStatusCode ResponseCode { get; set; }
public string ResponseResult { get; set; }
}
public class ErrorStateResponse
{
public IDictionary<string, string[]> ModelState { get; set; }
}
Time To Decode That Error Response Goodness
Now we need a couple of changes to the ClientBase
we introduced in the first article so we can decode the error response if it comes.
private static async Task<TResponse> CreateJsonResponse<TResponse>
(HttpResponseMessage response) where TResponse : ApiResponse, new()
{
var clientResponse = new TResponse
{
StatusIsSuccessful = response.IsSuccessStatusCode,
ErrorState = response.IsSuccessStatusCode ? null :
await DecodeContent<ErrorStateResponse>(response),
ResponseCode = response.StatusCode
};
if (response.Content != null)
{
clientResponse.ResponseResult = await response.Content.ReadAsStringAsync();
}
return clientResponse;
}
private static async Task<TContentResponse>
DecodeContent<TContentResponse>(HttpResponseMessage response)
{
var result = await response.Content.ReadAsStringAsync();
return Json.Decode<TContentResponse>(result);
}
Final Step: Stick It All In the MVC ModelState
That about covers the API side of things. Let’s make some changes to the client to handle any errors that come back. The POST
action now needs to check whether the response status was successful. If not, it adds the errors that come back to ModelState
. We’ll add this to a base controller in case we need it again. I've also added a standard anti-forgery token.
public class ProductController : BaseController
{
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> CreateProduct(ProductViewModel model)
{
var response = await productClient.CreateProduct(model);
if (response.StatusIsSuccessful)
{
var productId = response.Data;
return RedirectToAction("GetProduct", new { id = productId });
}
AddResponseErrorsToModelState(response);
return View(model);
}
}
public abstract class BaseController : Controller
{
protected void AddResponseErrorsToModelState(ApiResponse response)
{
var errors = response.ErrorState.ModelState;
if (errors == null)
{
return;
}
foreach (var error in errors)
{
foreach (var entry in
from entry in ModelState
let matchSuffix = string.Concat(".", entry.Key)
where error.Key.EndsWith(matchSuffix)
select entry)
{
ModelState.AddModelError(entry.Key, error.Value[0]);
}
}
}
}
So what's going on here? Recall that the model state errors are a little different between WebAPI and MVC? In our example, they look like this:
WebAPI Property |
MVC Property |
model.Name |
Name |
model.Description |
Description |
The LINQ statement above just looks for the MVC property name within the WebAPI
property name. If it finds it, it adds the error to that property in ModelState
. We match against .Name
(for example) in case we have properties like Name
and FirstName
. If we just matched against Name
, we’d be adding the Name
error against FirstName
.
Don't forget to add validation messages to the form in the CreateProduct
view. I won't cover that here but it's included in the VS solution download, available below.
Wrapping Up
We covered quite a bit here. Let’s recap:
- We returned a
BadRequest
response if the WebAPI ModelState
was invalid
- We then parsed the errors into an
ErrorStateResponse
in the ApiHelper
- For the final step, we added those errors to our MVC
ModelState
So What's Up Next?
In the next article, I'll be covering securing the WebAPI
project with simple, token-based authentication. Look out for it soon!
View the original article