Binding an HTTP Request into CLR Objects
The ASP.NET Web API framework reads each incoming HTTP request and creates the parameter objects for your action methods in the controller. This is one of the powerful features of the ASP.NET Web API framework that keeps the code in the controller classes free of the repetitive work related to HTTP infrastructure and helps you, the developer, focus on implementing the business requirements at hand. Table 5-1 shows the different types of bindings possible with ASP.NET Web API, the corresponding out-of-the-box class associated with the binding, the part of the request from which data is used for binding, the type of the parameter bound, and the extensibility options available.
The ASP.NET Web API framework reads the route data and the query string of the request URI and sets the parameters of the action methods that are simple types (primitives, DateTime, Guid, and so on) by a process called model binding. The parameters that are complex types are set based on the HTTP request body with the help of the media type formatters that we saw in Chapter 3. In Chapter 3, our focus was serialization: translation of CLR types into a format that can be transmitted over HTTP. In this chapter, we focus on deserialization: translation of an HTTP request message into CLR types. A media formatter can enlist itself in serialization and/or deserialization by returning true when the framework calls the CanWriteType(Type) and CanReadType(Type) methods respectively.
5.1 Reading the Raw HTTP Request
In this exercise, you will read the raw HTTP request without any help from the ASP.NET Web API framework. You will see that doing so is a lot of work and is error-prone. It is highly recommended to use the help that the ASP.NET Web API provides to read the requests so that you get nice, clean CLR objects to work with. This exercise is just to show you that it is possible to read the raw requests and help you appreciate the heavy lifting the framework does for you.
Listing 5-1. An Action Method Reading a Raw HTTP Request
using System;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Web.Http;
public class EmployeesController : ApiController
{
public void Post(HttpRequestMessage req)
{
var content = req.Content.ReadAsStringAsync().Result;
int id = Int32.Parse(req.RequestUri.Segments.Last());
Trace.WriteLine(content);
Trace.WriteLine(id);
}
}
{"Id":12345,"FirstName":"John","LastName":"Human"}
12345
In Listing 5-1, we read the request message content as a string using the ReadAsStringAsync method and take the ID from the URI. The code is naïve. It does not handle any of the rainy-day scenarios such as the ID not being a number, ID absent, and so on. Also, we get the request message content as JSON. We will need to parse this into a CLR object. If a client sends XML, we need to handle that as well. Obviously, it is a lot of painful work.
PARTIAL UPDATES
There is one good thing with taking control from ASP.NET Web API. In Chapter 1, we saw that partial updates are possible with PATCH using Delta<T>.
The challenge we have with partial updates is that when a request comes in without a value, it will be the same as if the request had the field present but with a null value. We now have the visibility to determine whether a request has a specific field absent or present but containing null without using Delta<T>. This is especially useful for partial updates. If a request comes in with a content of {"FirstName":"Jon"}, it basically means we need to update only the first name of the employee with ID of 12345 to Jon without touching any other properties. By leaving out the rest of the properties, a client can indicate that only the property in the request content must be updated.
So the request {"FirstName":"Jon"} can be distinguished from {"FirstName":"Jon", "LastName":null}. The first request asks for only the first name to be updated, and the second one asks for the first name to be updated to a new value and the last name to be cleared out. If you have used an object of type Employee as the parameter, you will not be able to distinguish these two requests, since in both the cases the LastName property will be set to null.
5.2 Reading the HTTP Request into a CLR Type
In this exercise, you will read the HTTP request with the help of the ASP.NET Web API framework. It is one step in the right direction yet you will see how beneficial it is.
Listing 5-2. An Action Method Reading a Raw HTTP Request into a CLR Type
public void Post(HttpRequestMessage req)
{
//var content = req.Content.ReadAsStringAsync().Result;
var content = req.Content.ReadAsAsync<Employee>().Result;
int id = Int32.Parse(req.RequestUri.Segments.Last());
Trace.WriteLine(content.Id);
Trace.WriteLine(content.FirstName);
Trace.WriteLine(content.LastName);
Trace.WriteLine(id);
}
Listing 5-3. The Employee Model Class
public class Employee
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
Listing 5-4. The Request XML
<Employee xmlns=" http://schemas.datacontract.org/2004/07/ RequestBinding.Models ">
<FirstName>John</FirstName>
<Id>12345</Id>
<LastName>Human</LastName>
</Employee>
Listing 5-5. Action Method Reading Raw HTTP Request into Parameters
public void Post(int id, Employee employee)
{
Trace.WriteLine(employee.Id);
Trace.WriteLine(employee.FirstName);
Trace.WriteLine(employee.LastName);
Trace.WriteLine(id);
}
This outputs the ID, first name, and last name of the employee from the request body, exactly the same as before. Regardless of the request media-type, XML or JSON, ASP.NET Web API correctly populates the parameter of type Employee as well as the int parameter. If a nonnumeric value is sent in the URI route, the framework catches it correctly, and you don't need to write code to handle any of those scenarios.
5.3 Binding the HTTP Request to Simple Types
In this exercise, you will see how ASP.NET Web API creates simple type parameters—the parameters of type such as primitives, DateTime, Guid, and so on—from the HTTP request message. By default, simple types are bound from the URI route data and query string.
Listing 5-6. An Action Method with Simple Type Parameters
public void Post(int id, string firstName, int locationId, Guid guid)
{
Trace.WriteLine(id);
Trace.WriteLine(firstName);
Trace.WriteLine(locationId);
Trace.WriteLine(guid);
}
12345
John
12
31c9359d-d332-4703-a896-7e9655eff171
You get a 404 - Not Found with an error message that No HTTP resource was found that matches the request URI. The reason for the 404 is that ASP.NET Web API is unable to bind the locationId parameter. There is nothing in the URI path or the query string to match the parameter, and the framework is unable to find the right action method for the request, so it fails with a 404. Though locationId field is present in the request body JSON, ASP.NET Web API does not try to bind it, because the action method parameters are simple types. By default, the simple types are bound from the URI path and query string only.
void Post(int id, string firstName, [FromBody]int locationId, Guid guid) { ... }
POST http://localhost:55778/api/employees/12345?firstName=John&guid=31c9359d-d332-4703-a896-7e9655eff171 HTTP/1.1
Content-Type: application/json
Host: localhost:55778
Content-Length: 2
12
The framework starts binding the values correctly.
THE WHOLE BODY BINDING
In the preceding exercise, we had to pass the integer 12 in the request body because of the way ASP.NET Web API binding is designed. The whole request body, not a piece of the body, is bound to a parameter. For this reason, you cannot have multiple parameters with the FromBody attribute in an action method. If that is the need, use a complex type such as a class with properties equivalent to the parameters. For example, instead of using [FromBody]int locationId, [FromBody]string locationName, use a class, as follows.
public class LocationDto
{
public int LocationId {get; set;}
public string LocationName {get; set;}
}
5.4 Binding the HTTP Request to Complex Types
By default, complex types are bound from the request message body and simple types are bound from the URI path and the query string. In Exercise 5.3, you saw how ASP.NET Web API binds a simple type from the URI and query string. Then we changed this default behavior by applying the FromBody attribute on a simple type parameter to let the framework bind the message body to a simple type.
In this exercise, you will see how a complex type parameter is bound from the HTTP request message. It is the default behavior of ASP.NET Web API to bind the request body to a complex type but just as we used the FromBody attribute to change that behavior, in this exercise, we will use the FromUri attribute to bind a complex type from the URI and query string.
ASP.NET Web API uses media formatters to bind the request body to complex types. Chapter 3 covered media type formatters from the perspective of the response message being mapped to a CLR object. In this chapter, we again see the media formatters in action but for mapping the request to a CLR object.
Figure 5-1. The POST method of EmployeesController
Figure 5-2. POST method parameters when the breakpoint is hit
<Employee xmlns=" http://schemas.datacontract.org/2004/07/RequestBinding.Models ">
<FirstName>John</FirstName>
<Id>12345</Id>
<LastName>Human</LastName>
</Employee>
Everything works fine so far because the action method parameter Employee is a complex type and ASP.NET Web API binds the parameter from the request body with the help of formatters based on the content type. Let us now change the request data coming in the body to a query string and see how it works.
void Post(int id, [FromUri]Employee employee) { ... }
It works now with the Employee parameter being populated from the query string.
THE BODY CAN BE READ ONLY ONCE
The request body is a non-rewindable stream; it can be read only once. Modify the POST action method, as shown in the following code. It now takes two parameters: the first is the raw HTTP request and the second is Employee type.
public void Post(int id, HttpRequestMessage req, Employee employee)
{
var content = req.Content.ReadAsStringAsync().Result;
var employeeContent = req.Content.ReadAsAsync<Employee>().Result;
} // Place the break point here
Place a breakpoint on the ending curly brace. Rebuild the solution and run the project. Make a POST to http://localhost:<port>/api/employees/12345 from Fiddler with Content-Type: application/json in the Request Headers text box of the Composer tab and the JSON {"Id":12345,"FirstName":"John","LastName":"Human"} in the Request Body text box.
When the breakpoint is hit, inspect the variables content and employeeContent. The values are an empty string and null, respectively. Inspect the Employee type parameter. You will see that it is deserialized correctly with all the data from the request body. The reason we cannot read the HTTP request content inside the action method is that the stream can be read only once. The formatter has already read the stream and populated the Employee parameter object. So we are not able to read the stream again. This is an important point to understand.
5.5 Binding the HTTP Request to a Collection
In this exercise, you will see how a collection parameter such as List<T> is bound from the HTTP request message. List<T> is a complex type and by default, complex types are bound from the request message body.
Listing 5-7. The Action Method with a Collection Parameter
public void Post(int id, List<string> nickNames)
{
Trace.WriteLine(String.Join(", ", nickNames));
}
This throws an exception, since the nickNames parameter is null this time around. Since List<string> is a complex type, ASP.NET Web API tries to bind the request body to this parameter. Since the request body is empty, the nickNames parameter is null.
An HTML form is a section of a document containing normal content, markup, special elements called controls (text boxes, checkboxes, radio buttons and so on), and labels on those controls. A form is submitted to a server-side program that processes the user-entered data.
When a web browser sends an HTTP POST of an HTML form, it specifies a media type of application/x-www-form-urlencoded. The data in the HTML form is sent as name-value pairs. For example, take the case of the HTML form shown in Listing 5-8.
Listing 5-8. An HTML Form
<form action=" http://localhost:55778/api/employees " method="post">
<div>
<label for="firstname">First Name:</label>
<input type="text" name="firstname"/>
</div>
<div>
<label for="lastname">Last Name:</label>
<input type="text" name="lastname"/>
</div>
<input type="submit" value="Submit"/>
</form>
When I enter John in the First Name text box and Human Being in the Last Name text box and click Submit, the browser I use, Internet Explorer, posts the form data to the URI that I have specified in the action attribute of the form element. Listing 5-9 shows the HTTP request message (with some of the headers removed for brevity). The data I entered are encoded and sent in the form of name-value pairs.
Listing 5-9. Form Data
POST http://localhost:55778/api/employees HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Host: localhost:55778
Content-Length: 35
firstname=John& lastname=Human+Being
Form-URL-encoded data is a widely used media type for submitting data from a browser to a server-side program. ASP.NET Web API is capable of handling requests with form-URL-encoded data. However, it is important to note that a browser posting a form directly to an ASP.NET Web API endpoint as in Listing 5-9 is not a likely scenario, for the simple reason that ASP.NET Web API does not typically produce HTML that can be rendered by the browser directly. The most likely use case for the form-URL-encoded data will be a JavaScript library such as jQuery posting a form through an AJAX call, or a client-side library used by a native application formatting user-entered data as name-value pairs and posting them to ASP.NET Web API as form-URL-encoded data.
ASP.NET Web API can bind form-URL-encoded data to a special class FormDataCollection in the System.Net.Http.Formatting namespace. It can also bind the form-URL-encoded data to custom classes like our Employee class. In the former case, the out-of-the-box media formatter FormUrlEncodedMediaTypeFormatter is used and in the latter case, JQueryMvcFormUrlEncodedFormatter is used.
5.6.1 Binding to FormDataCollection
In this exercise, you will see how ASP.NET Web API binds form-URL-encoded data to FormDataCollection . I start by showing you the media type formatters that handle the form-URL-encoded data (application/x-www-form-urlencoded).
Listing 5-10. Listing Media Formatters
using System.Diagnostics;
using System.Linq;
using System.Net.Http.Formatting;
using System.Web.Http;
using RequestBinding.Models;
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
foreach (var formatter in config.Formatters.Where(f => f.SupportedMediaTypes
.Any(m => m.MediaType.Equals(
"application/x-www-form-urlencoded"))))
{
Trace.WriteLine(formatter.GetType().Name);
Trace.WriteLine(" CanReadType Employee: " + formatter
.CanReadType(typeof(Employee)));
Trace.WriteLine(" CanWriteType Employee: " + formatter
.CanWriteType(typeof(Employee)));
Trace.WriteLine(" CanReadType FormDataCollection: " +
formatter
.CanReadType(
typeof(FormDataCollection)));
Trace.WriteLine(" CanWriteType FormDataCollection: " +
formatter
.CanWriteType(
typeof(FormDataCollection)));
Trace.WriteLine(" Base: " + formatter.GetType().BaseType.Name);
Trace.WriteLine(" Media Types: " +
string.Join(", ", formatter.SupportedMediaTypes));
}
}
}
Listing 5-11. Media Type Formatters Supporting Form Data
FormUrlEncodedMediaTypeFormatter
CanReadType Employee: False
CanWriteType Employee: False
CanReadType FormDataCollection: True
CanWriteType FormDataCollection: False
Base: MediaTypeFormatter
Media Types: application/x-www-form-urlencoded
JQueryMvcFormUrlEncodedFormatter
CanReadType Employee: True
CanWriteType Employee: False
CanReadType FormDataCollection: True
CanWriteType FormDataCollection: False
Base: FormUrlEncodedMediaTypeFormatter
Media Types: application/x-www-form-urlencoded
There are two out-of-the-box formatters that support the media type application/x-www-form-urlencoded:
Neither of these two formatters serializes CLR types into the media type of application/x-www-form-urlencoded, as you can infer from the false returned by the CanWriteType method.
Listing 5-12. The Revised POST Action Method
using System.Diagnostics;
using System.Net.Http.Formatting;
using System.Web.Http;
public class EmployeesController : ApiController
{
public void Post( FormDataCollectiondata)
{
Trace.WriteLine(data.Get("firstName"));
Trace.WriteLine(data.Get("lastName"));
}
}
Listing 5-13. Home/IndexView
<form action="/api/employees" method="post">
<div>
<label for="firstname">First Name:</label>
<input type="text" name="firstname"/>
</div>
<div>
<label for="lastname">Last Name:</label>
<input type="text" name="lastname"/>
</div>
<input type="submit" value="Submit"/>
</form>
Listing 5-14. Installing the NuGet Package
Install-Package Microsoft.AspNet.WebApi.Tracing
Listing 5-15. Enabling Tracing
config.EnableSystemDiagnosticsTracing();
Listing 5-16. Trace Output
Message='Employees', Operation=DefaultHttpControllerSelector.SelectController
Message='HelloWebApi.Controllers.EmployeesController',
Operation=DefaultHttpControllerActivator.Create
Message='HelloWebApi.Controllers.EmployeesController',
Operation=HttpControllerDescriptor.CreateController
Message='Selected action 'Post(FormDataCollection data)'',
Operation=ApiControllerActionSelector.SelectAction
Message='Value read='System.Net.Http.Formatting.FormDataCollection'',Operation= FormUrlEncodedMediaTypeFormatter.ReadFromStreamAsync
Message='Parameter 'data' bound to the value 'System.Net.Http.Formatting.FormDataCollection'',
Operation=FormatterParameterBinding.ExecuteBindingAsync
Message='Model state is valid. Values: data=System.Net.Http.Formatting.FormDataCollection',
Operation=HttpActionBinding.ExecuteBindingAsync
Message='Action returned 'null'',Operation=ReflectedHttpActionDescriptor.ExecuteAsync
Operation=ApiControllerActionInvoker.InvokeActionAsync, Status=204 (NoContent)
Operation=EmployeesController.ExecuteAsync, Status=204 (NoContent)
Response, Status=204 (NoContent), Method=POST, Url= http://localhost:55778/api/employees ,
Message='Content-type='none', content-length=unknown'
Operation=CultureHandler.SendAsync, Status=204 (NoContent)
Operation=EmployeesController.Dispose
This exercise is similar to Exercise 5.6.1, but instead of using the FormDataCollection parameter, it uses a custom model class Employee that we have been using all along in this book as the action method parameter.
Listing 5-17. POST Action Method Using a Complex Type Parameter
public int Post( Employeeemployee)
{
return new Random().Next();
}
FormatterParameterBinding ExecuteBindingAsync Binding parameter 'employee'
JQueryMvcFormUrlEncodedFormatter ReadFromStreamAsync Type='Employee',
content-type='application/x-www-form-urlencoded' JQueryMvcFormUrlEncodedFormatter
ReadFromStreamAsync Value read='HelloWebApi.Models.Employee'
In the previous steps we posted an HTML form from the browser by clicking the Submit button and making a web API call. This was only for illustration. In practice, you will never be posting by submitting a page. Instead, you will use a client-side script library like jQuery. Let us now modify our page to submit the form using jQuery AJAX.
Listing 5-18. /Home/Index View Making an Ajax Call
@section scripts{
<script type="text/javascript">
$.support.cors = true; // Needed for IE
$(document).ready(function () {
$('#btnPost').click(function () {
$.post(" http://localhost:55778/api/employees ",
$('#employeeForm').serialize())
.done(function (data) { alert("New employee created. ID is " + data); })
.fail(function () { alert("Error creating employee"); });
});
});
</script>
}
<form id="employeeForm" action=" http://localhost:37276/api/employees " method="post">
<div>
<label for="firstname">First Name:</label>
<input type="text" name="firstname" />
</div>
<div>
<label for="lastname">Last Name:</label>
<input type="text" name="lastname" />
</div>
<input type="submit" value="Submit" />
<input id="btnPost" type="button" value="jQuery POST" />
</form>
Request |
POST http://localhost:55778/api/employees HTTP/1.1 Accept: */* Content-Type: application/x-www-form-urlencoded X-Requested-With: XMLHttpRequestContent-Length: 35 firstname=John&lastname=Human+Being |
Response |
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Content-Length: 9 544421176 |
5.6.3 Binding to a Simple Type
ASP.NET Web API can bind the form-URL-encoded data into a simple type parameter as well. We have seen in the previous exercises that the [FromBody] attribute will need to be applied to an action method parameter to let ASP.NET Web API bind the request body to a simple type. In Exercise 5.6.2, to bind to a complex type (our Employee type), we sent two name-value pairs: firstname=John&lastname=Human+Being. In the case of binding to a simple type, only one value can be sent, since the body must be used as a whole for binding. Also, a name-value pair cannot be sent; the value prefixed with an equal sign is all that can be sent.
Listing 5-19. POST Action Method Accepting a Simple Type
public void Post([ FromBody]string lastName)
{
Trace.WriteLine(lastName);
}
POST http://localhost:55778/api/employees HTTP/1.1
Host: localhost:55778
Content-Type: application/x-www-form-urlencoded
Content-Length: 12
=Human+Being
You can also use jQuery to submit a single value, as shown in Listing 5-20.
Listing 5-20. jQuery Posting a Single Value (/Home/Index View)
@section scripts{
<script type="text/javascript">
$.support.cors = true; // Needed for IE
$(document).ready(function () {
$('#btnPost').click(function () {
$.post(" http://localhost:55778/api/employees ",
$('#employeeForm').serialize())
.done(function (data) { alert("New employee created. ID is " + data); })
.fail(function () { alert("Error creating employee"); });
});
$('#btnPostPartial').click(function () {
$.post(" http://localhost:55778/api/employees ",
{ "": $('#lastname').val() })
.done(function (data) { alert("Success"); })
.fail(function () { alert("Error creating employee"); });
});
});
</script>
}
<form id="employeeForm" action=" http://localhost:55778/api/employees " method="post">
<div>
<label for="firstname">First Name:</label>
<input type="text" id="firstname" name="firstname" />
</div>
<div>
<label for="lastname">Last Name:</label>
<input type="text" id="lastname"name="lastname" />
</div>
<input type="submit" value="Submit" />
<input id="btnPost" type="button" value="jQuery POST" />
<input id="btnPostPartial" type="button" value="jQuery POST Partial" />
</form>
Note ASP.NET Web API can bind form-URL-encoded data to a collection such as List<int>, which is a complex type. By default, ASP.NET Web API binds the request body to a complex type and hence there is no need for the [FromBody] attribute. For example, if you have an action method public void Post(List<int> numbers) {}, by making an HTTP POST with the request body of =1&=2&=3&=4, you can make ASP.NET Web API set the numbers parameter to a list of four integers: 1, 2, 3, and 4.
5.7 Binding dd/MM/yyyy Dates
In this exercise, you will see how to make ASP.NET Web API bind the message content to a DateTime parameter. By default, ASP.NET Web API returns the date in ISO 8601 format; for example 2nd June 1998 is 1998-06-02T00:00:00. If the incoming request payload contains the date in the same format, the Web API will have no trouble correctly binding the parameter. When the client sends a date in some other format, say dd/MM/yyyy, a bit more work is required. This exercise is related to Exercise 3.4.3, Internationalizing the Dates, which demonstrated the formatting of the date into the response content This exercise covers deserialization; that is, binding the date from the request message to a DateTime parameter.
Listing 5-21. Employee Class with Date of Joining
using System;
namespace RequestBinding.Models
{
public class Employee
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime Doj { get; set; }
}
}
Listing 5-22. Action Method Handling POST
public void Post(Employee employee)
{
// Do Nothing
}
{"Id":12345,"FirstName":"John","LastName":"Human","Doj": "06/02/1998"}
ASP.NET Web API has again correctly bound the request to the Doj property, which is set to June 02, 1998. Of course, the correctness depends on which side of the Atlantic you are on! I have regional settings of English (United States) in my computer, so this interpretation is not surprising. But if you are used to dd/MM/yyyy format, it is not correct and you will be expecting February 06, 1998 instead.
Listing 5-23. Registering the Value Provider
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Formatters.JsonFormatter .SerializerSettings.Culture = new System.Globalization .CultureInfo("en-GB");
}
}
This is great as long as your application can work with only one culture. It is not a scalable solution if you must handle multiple cultures at the same time on a per-request basis. As an alternative, we saw the request header Accept-Language being used for localization in the exercises in Chapter 4. We can use the same header to decide on the fly how the model should be bound.
Listing 5-24. The DateTimeConverter Class
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
public class DateTimeConverter : DateTimeConverterBase
{
public override object ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
return DateTime.Parse(reader.Value.ToString());
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(((DateTime)value).ToString());
}
}
Listing 5-25. A Culture Message Handler
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
public class CultureHandler : DelegatingHandler
{
private ISet<string> supportedCultures = new HashSet<string>() { "en-us", "en", "fr-fr", "fr" };
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var list = request.Headers.AcceptLanguage;
if (list != null && list.Count > 0)
{
var headerValue = list.OrderByDescending(e => e.Quality ?? 1.0D)
.Where(e => !e.Quality.HasValue ||
e.Quality.Value > 0.0D)
.FirstOrDefault(e => supportedCultures
.Contains(e.Value, StringComparer.OrdinalIgnoreCase));
// Case 1: We can support what client has asked for
if (headerValue != null)
{
Thread.CurrentThread.CurrentUICulture =
CultureInfo.GetCultureInfo(headerValue.Value);
Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture;
}
// Case 2: Client is okay to accept anything we support except
// the ones explicitly specified as not preferred by setting q=0
if (list.Any(e => e.Value == "*" &&
(!e.Quality.HasValue || e.Quality.Value > 0.0D)))
{
var culture = supportedCultures.Where(sc =>
!list.Any(e =>
e.Value.Equals(sc, StringComparison.OrdinalIgnoreCase) &&
e.Quality.HasValue &&
e.Quality.Value == 0.0D))
.FirstOrDefault();
if (culture != null)
{
Thread.CurrentThread.CurrentUICulture =
CultureInfo.GetCultureInfo(culture);
Thread.CurrentThread.CurrentCulture = Thread.CurrentThread.CurrentUICulture;
}
}
}
return await base.SendAsync(request, cancellationToken);
}
}
Listing 5-26. WebApiConfig with Converter and Handler Added
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
//config.Formatters.JsonFormatter
// .SerializerSettings.Culture = new System.Globalization.CultureInfo("en-GB");
config.Formatters.JsonFormatter
.SerializerSettings
.Converters.Add(new DateTimeConverter());
config.MessageHandlers.Add(new CultureHandler());
}
}
In this exercise, you will use a custom TypeConverter to convert a single query string parameter to a complex type, which is a custom class named Shift. The Shift class has a DateTime property, which needs to be bound correctly from the query string parameter. Since the data is read from the query string, the binding type is model binding. In the previous exercise, the data was read from the request body and hence it was parameter binding.
For this exercise, our use case is to get a list of employees based on a shift on a given day. A shift starts and ends the same day. So the search criteria model, which is the Shift class, has a DateTime component which stores the day and two TimeSpan components representing the start and end times. You can model this in multiple ways, but that is not the point of this exercise. Assume this will be the model to which the query string data must be bound.
The query string value is in the form of a date, followed by a T, followed by hours and minutes of the start time, followed by a T and then the hours and minutes of the end time. For example, a request with Accept-Language: en-us with query string of shift=06/02/2012T0800T1700 needs to be bound to a Shift object with a Date of June the 2nd and Start and End properties of 8 hours and 17 hours respectively. When the Accept-Language header is fr-fr, the date will be February the 6th.
Listing 5-27. The Shift Model
public class Shift
{
public DateTime Date { get; set; }
public TimeSpan Start { get; set; }
public TimeSpan End { get; set; }
}
Listing 5-28. The GET Action Method
public HttpResponseMessage Get(Shift shift)
{
// Do something with shift
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("")
};
return response;
}
Listing 5-29. The ShiftTypeConverter Class
using System;
using System.ComponentModel;
using System.Globalization;
using RequestBinding.Models;
public class ShiftTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
return true;
return base.CanConvertFrom(context, sourceType);
}
public override object ConvertFrom(ITypeDescriptorContext context,
CultureInfo culture, object value)
{
if (value is string)
{
var parts = ((string)value).Split('T'),
DateTime date;
if (DateTime.TryParse((string)parts[0], out date))
{
return new Shift()
{
Date = date,
Start = parts[1].ToTimeSpan(),
End = parts[2].ToTimeSpan()
};
}
}
return base.ConvertFrom(context, culture, value);
}
}
Listing 5-30. The ToTimeSpan Extension Method
public static class TimeHelper
{
public static TimeSpan ToTimeSpan(this string time)
{
int hour = Int32.Parse(time.Substring(0, 2));
int min = Int32.Parse(time.Substring(2, 2));
return new TimeSpan(hour, min, 0);
}
}
Listing 5-31. The Shift Class with TypeConverter
using System;
using System.ComponentModel;
[TypeConverter(typeof(ShiftTypeConverter))]
public class Shift
{
public DateTime Date { get; set; }
public TimeSpan Start { get; set; }
public TimeSpan End { get; set; }
}
We are all set. For the converter to read and parse the date correctly, we need Thread.CurrentThread.CurrentCulture to be set correctly to the locale sent in the Accept-Language header. The CultureHandler method we saw earlier takes care of this for you when plugged into the pipeline.
Now, the Shift parameter is correctly set, with the Date property set to June the 2nd.
We saw in Exercise 5.4 earlier that a complex type can be bound from a query string. What is the difference here? In Exercise 5.4, the complex type was mapped from multiple query string parameters, with property names matching exactly the query string parameter field name. But in this exercise, we converted the value from one query string parameter into a complex Shift type, hence the use of TypeConverter.
5.9 Creating a Custom Value Provider
In the preceding exercises of this chapter, I showed how the ASP.NET Web API framework binds the incoming HTTP request message, specifically the route data and the query string part of the URI. This is called model binding, the same concept you will be familiar with if you have ASP.NET MVC experience. There are two entities in action here: the model binder and the value provider . The job of a value provider is to get data from the HTTP request message and feed the values into a model binder to build the model. The value provider abstraction ensures that a model binder is decoupled from the details of the request message. There are two extensibility options associated with model binding:
The value provider reads a value and maps that to a simple type, typically without any conversion. The model binder builds a model, typically a complex type, based on the values provided by the value providers. Though it is possible to build a model based on entities outside of the request message URI, it is typical for a model binder to limit the scope to the request URI. After all, model binding is limited to the request URI. For binding a model based on entities outside the URI and even outside the request itself, you can use parameter binding.
Note A custom value provider or a model binder is applicable only for model binding. If you have experience with ASP.NET MVC, where there is only model binding, it might be difficult at times to realize that model binding is for the URI path and query string only and not for the message body, which is bound by formatter binding.
In this exercise, you will create a custom value provider that gets the value from an HTTP header. To create a new custom value provider, you need to implement the IValueProvider interface and subclass the abstract class ValueProviderFactory.
By default, the model binding acts on the URI path and query, while the formatter binding acts on the request message body. There is no binder out of the box for reading headers. So this exercise shows the steps to create a value provider that does the same thing. A value provider does nothing fancy. As the name indicates, it just provides a value based on some part of the HTTP request message, for the out-of-the-box binders or your own custom binder to bind the values to a model, which could very well be a complex type. A value provider, however, just provides value for simple types.
Before we start writing code, it is important to understand that a request can contain multiple headers of same name but with different values. Not all, but some of them can. The multiple values can also be put into one header with a comma separating them. For example, the following two requests are the same and are valid.
Request 1
GET http://localhost:55778/api/employees HTTP/1.1
Host: localhost:55778
Accept: application/xml; q=0.2
Accept: application/json; q = 0.3
Request 2
GET http://localhost:55778/api/employees HTTP/1.1
Host: localhost:55778
Accept: application/xml; q=0.2, application/json; q = 0.3
Our custom value provider must handle these cases. If there are multiple values, they will be bound to a list, as in IEnumerable<T>. If one value, it will be bound to just a simple type. Another point for consideration is the naming convention. HTTP headers typically contain hyphens, while C# variables do not. The value provider must be able to ignore the hyphens and retrieve the value from the header.
Get([ValueProvider(typeof(HeaderValueProviderFactory))]IEnumerable<string> ifmatch)
Listing 5-32. The GET Action Method with ModelBinder Applied to the Parameter
public HttpResponseMessage Get(
[System.Web.Http.ModelBinding.ModelBinder]IEnumerable<string> ifmatch)
{
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(ifmatch.First().ToString())
};
return response;
}
Listing 5-33. A Custom Value Provider Class
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http.Headers;
using System.Web.Http.ValueProviders;
public class HeaderValueProvider : IValueProvider
{
private readonly HttpRequestHeaders headers;
// The function to test each element of the header, which is a KeyValuePair
// for matching key ignoring the dashes. For example, the If-Match header
// will be chosen if the parameter is defined with a name ifmatch, ifMatch, etc.
private Func<KeyValuePair<string, IEnumerable<string>>, string, bool> predicate =
(header, key) =>
{
return header.Key.Replace("-", String.Empty)
.Equals(key, StringComparison.OrdinalIgnoreCase);
};
public HeaderValueProvider(HttpRequestHeaders headers)
{
this.headers = headers;
}
public bool ContainsPrefix(string prefix)
{
return headers.Any(h => predicate(h, prefix));
}
public ValueProviderResult GetValue(string key)
{
var header = headers.FirstOrDefault(h => predicate(h, key));
if (!String.IsNullOrEmpty(header.Key))
{
key = header.Key; // Replace the passed-in key with the header name
var values = headers.GetValues(key);
if (values.Count() > 1) // We got a list of values
return new ValueProviderResult(values, null, CultureInfo.CurrentCulture);
else
{
// We could have received multiple values (comma separated) or just one value
string value = values.First();
values = value.Split(',').Select(x => x.Trim()).ToArray();
if (values.Count() > 1)
return new ValueProviderResult(values, null, CultureInfo.CurrentCulture);
else
return new ValueProviderResult(value, value, CultureInfo.CurrentCulture);
}
}
return null;
}
}
Listing 5-34. A Value Provider Factory
using System.Web.Http.Controllers;
using System.Web.Http.ValueProviders;
public class HeaderValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(HttpActionContext actionContext)
{
var request = actionContext.ControllerContext.Request;
return new HeaderValueProvider(request.Headers);
}
}
Listing 5-35. Registering the Value Provider
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
//config.Formatters.JsonFormatter
// .SerializerSettings.Culture = new System.Globalization.CultureInfo("en-GB");
config.Services.Add(typeof(
System.Web.Http.ValueProviders.ValueProviderFactory),
new HeaderValueProviderFactory());
config.Formatters.JsonFormatter
.SerializerSettings
.Converters.Add(new DateTimeConverter());
config.EnableSystemDiagnosticsTracing();
config.MessageHandlers.Add(new CultureHandler());
}
}
If-Match:hello
If-Match:world
Note At this point, you might wonder why we bother creating a value provider, when the headers can be directly read from the request in the action method. First of all, the value provider encapsulates the logic of getting the value from the header. We can reuse it in all the places we need the data from the headers. Second, the action method has no dependency on the request object, and unit-testing the action method will be easier.
5.10 Creating a Custom Model Binder
In this exercise, you will create a custom model binder. We saw in the preceding exercises that a type converter reads a value and performs some conversion steps before creating a complex type, whereas a value provider reads a value and maps that to a simple type, typically without any conversion. A model binder, on the other hand, builds a model, typically a complex type, based on the values provided by one or more value providers.
To understand the custom model binder, consider an analogy. If a chef is making the Italian dessert tiramisu, she needs ladyfingers (Savoiardi). She need not make the ladyfingers herself. The chef can have someone else provide it, and she can concentrate on the core business of making tiramisu using the ladyfingers and other ingredients. Now, the chef is the model binder. Whoever provides the ladyfingers is the value provider, and the value is the ladyfinger. The model binder creates the model, which is tiramisu! (This is just an analogy though, so don't read too much into it!)
Let us get on with creating the custom model binder. The URI from which our custom model binder will extract data is http://localhost:55778/api/employees?dept=eng&dept=rch&doj=06/02/2012. The request message has a custom header X-CTC-Based, which will need to be bound to the model as well. We will create a custom model binder that will cook up a nice single model based on the URI as well as the header.
Listing 5-36. The GET Action Method with Custom Model Binder
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.ModelBinding;
using RequestBinding.Models;
public class EmployeesController : ApiController
{
public HttpResponseMessage Get(
[ModelBinder(typeof(TalentScoutModelBinderProvider))]
TalentScout scout)
{
// Do your logic with scout model
var response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent("")
};
return response;
}
}
Listing 5-37. The TalentScout Model
public class TalentScout
{
public IList<string> Departments { get; set; }
public bool IsCtcBased { get; set; }
public DateTime Doj { get; set; }
}
Listing 5-38. A Binder Provider
using System;
using System.Web.Http;
using System.Web.Http.ModelBinding;
public class TalentScoutModelBinderProvider : ModelBinderProvider
{
public override IModelBinder GetBinder(HttpConfiguration configuration, Type modelType)
{
return new TalentScoutModelBinder();
}
}
Listing 5-39. A Custom Model Binder
using System;
using System.Linq;
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;
using RequestBinding.Models;
public class TalentScoutModelBinder : IModelBinder
{
public bool BindModel(HttpActionContext actionContext, ModelBindingContext context)
{
var scoutCriteria = (TalentScout)context.Model ?? new TalentScout();
var result = context.ValueProvider.GetValue("dept");
if (result != null)
scoutCriteria.Departments = result.AttemptedValue
.Split(',')
.Select(d => d.Trim()).ToList();
result = context.ValueProvider.GetValue("xctcbased");
if (result != null)
{
int basedOn;
if (Int32.TryParse(result.AttemptedValue, out basedOn))
{
scoutCriteria.IsCtcBased = (basedOn > 0);
}
}
result = context.ValueProvider.GetValue("doj");
if (result != null)
{
DateTime doj;
if (DateTime.TryParse(result.AttemptedValue, out doj))
{
scoutCriteria.Doj = doj;
}
}
context.Model = scoutCriteria;
return true;
}
}
5.11 Creating a Custom Parameter Binder
The two major types of parameter binding in ASP.NET Web API are model binding and formatter binding, respectively represented by the classes ModelBinderParameterBinding and FormatterParameterBinding. You've seen that model binding binds request URI parts to the action method parameter (simple types, by default) whereas formatter binding binds the request body to the action method parameter (complex types, by default) using media type formatters. Both ModelBinderParameterBinding and FormatterParameterBinding derive from the abstract class HttpParameterBinding.
It is possible to create your own parameter binder by subclassing this abstract class. Typically, a custom parameter binder is created only when the out-of-the-box binding (even after customizing using the extensibility points) is not able to meet your requirements. One good example use case for a custom parameter binder is binding a value that is totally unrelated to the request message. Another example is building your model from multiple request parts: URI, request body, and headers. Out of the box, you can bind from either URI or the request message body but not both. In this exercise, You'll see how to create a custom parameter binder that builds a model from all the three parts: URI, request body, and headers.
For the purpose of this exercise, the incoming request is a PUT request, shown in Listing 5-40.
Listing 5-40. A Request Message
PUT http://localhost:55778/api/employees/12345?doj=6/2/1998 HTTP/1.1
Host: localhost:55778
Content-Length: 41
Content-Type: application/json
X-Affiliation: Green Planet
{"FirstName":"John", "LastName":"Human" }
The model we will bind this request to is the Employee class. The mapping of the request parts to the model will happen as follows:
Take the following steps to create and use a custom parameter binder:
Listing 5-41. A PUT Action Method
public void Put(int id, Employee employee)
{
// Does nothing!
}
Listing 5-42. The Employee Class
public class Employee
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime Doj { get; set; }
public string Xaffiliation { get; set; }
}
Listing 5-43. The AllRequestParameterBinding Class (Incomplete)
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Metadata;
using System.Web.Http.ModelBinding;
using RequestBinding.Models;
public class AllRequestParameterBinding : HttpParameterBinding
{
private HttpParameterBinding modelBinding = null;
private HttpParameterBinding formatterBinding = null;
public AllRequestParameterBinding(HttpParameterDescriptor descriptor)
: base(descriptor)
{
// GetBinding returns ModelBinderParameterBinding
modelBinding = new ModelBinderAttribute().GetBinding(descriptor);
// GetBinding returns FormatterParameterBinding
formatterBinding = new FromBodyAttribute().GetBinding(descriptor);
}
// other methods go here
}
Listing 5-44. The ExecuteBindingAsync Method
public override async Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
HttpActionContext context,
CancellationToken cancellationToken)
{
// Perform formatter binding
await formatterBinding.ExecuteBindingAsync(metadataProvider, context, cancellationToken);
// and store the resulting model
var employee = GetValue(context) as Employee;
// Perform model binding
await modelBinding.ExecuteBindingAsync(metadataProvider, context, cancellationToken);
// and store the resulting model
var employeeFromUri = GetValue(context) as Employee;
// Apply the delta on top of the employee object resulting from formatter binding
employee = Merge(employee, employeeFromUri);
// Set the merged model in the context
SetValue(context, employee);
}
Listing 5-45. The Merge Method
private Employee Merge(Employee @base, Employee @new)
{
Type employeeType = typeof(Employee);
foreach (var property in employeeType.GetProperties(
BindingFlags.Instance | BindingFlags.Public))
{
object baseValue = property.GetValue(@base, null);
object newValue = property.GetValue(@new, null);
object defaultValue = property.PropertyType.IsValueType ?
Activator.CreateInstance(property.PropertyType) :
null;
if(baseValue == null || baseValue.Equals(defaultValue))
property.SetValue(@base, newValue);
}
return @base;
}
Listing 5-46. Registering the Binder
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
config.Services.Add(typeof(System.Web.Http.ValueProviders.ValueProviderFactory),
new HeaderValueProviderFactory());
config.Formatters.JsonFormatter
.SerializerSettings
.Converters.Add(new DateTimeConverter());
config.EnableSystemDiagnosticsTracing();
config.MessageHandlers.Add(new CultureHandler());
var rules = config.ParameterBindingRules;
rules.Insert(0, p =>
{
if (p.ParameterType == typeof(Employee))
{
return new AllRequestParameterBinding(p);
}
return null;
});
}
}
5.12 Creating a Custom Media Formatter
In Chapter 3, we created a custom media formatter for serialization. In this exercise, you will extend the same formatter to bind the request body, which is the fixed-width text, to the action method's Employee type parameter.
The incoming fixed-width text request will take this format: Employee ID will be 6 digits and zero-prefixed, followed by the first name and the last name. Both the names will have a length of 20 characters padded with trailing spaces to ensure the length. Thus, a record for an employee John Human with ID of 12345 will be 012345John<followed by 16 spaces>Human<followed by 15 spaces>.
Listing 5-47. The FixedWidthTextMediaFormatter Class
public class FixedWidthTextMediaFormatter : MediaTypeFormatter
{
public FixedWidthTextMediaFormatter()
{
SupportedEncodings.Add(Encoding.UTF8);
SupportedEncodings.Add(Encoding.Unicode);
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
}
public override bool CanReadType(Type type)
{
//return false;
return typeof(Employee) == type;
}
public override bool CanWriteType(Type type)
{
return typeof(IEnumerable<Employee>)
.IsAssignableFrom(type);
}
public override async Task WriteToStreamAsync(
Type type,
object value,
Stream stream,
HttpContent content,
TransportContext transportContext)
{
using (stream)
{
Encoding encoding = SelectCharacterEncoding(content.Headers);
using (var writer = new StreamWriter(stream, encoding))
{
var employees = value as IEnumerable<Employee>;
if (employees != null)
{
foreach (var employee in employees)
{
await writer.WriteLineAsync(
String.Format("{0:000000}{1,-20}{2,-20}",
employee.Id,
employee.FirstName,
employee.LastName));
}
await writer.FlushAsync();
}
}
}
}
// ReadFromStreamAsync method goes here
}
Listing 5-48. The ReadFromStreamAsync Method
public async override Task<object> ReadFromStreamAsync(
Type type,
Stream readStream,
HttpContent content,
IFormatterLogger formatterLogger)
{
using (readStream)
{
Encoding encoding = SelectCharacterEncoding(content.Headers);
using (var reader = new StreamReader(readStream, encoding))
{
string messageBody = await reader.ReadToEndAsync();
var employee = new Employee();
employee.Id = Int32.Parse(messageBody.Substring(0, 6));
employee.FirstName = messageBody.Substring(6, 20).Trim();
employee.LastName = messageBody.Substring(26, 20).Trim();
return employee;
}
}
}
Listing 5-49. Adding the Formatter to the Collection
config.Formatters.Add(
new FixedWidthTextMediaFormatter());
Request |
PUT http://localhost:55778/api/employees/12345 HTTP/1.1 Host: localhost:55778 Content-Type: text/plain;charset=utf-16 Content-Length: 92 012345John Human |
Response | HTTP/1.1 204 No Content |
Note If you leave out charset=utf-16 from the Content-Type header, you will not see any difference other than the encoding returned by the SelectCharacterEncoding method changing to UTF-8. Since we use all English characters and numbers (ASCII characters), it makes no difference, but it is always the best practice to respect the charset in Content-Type header of the request and read the request body accordingly. When you start dealing with charsets corresponding to other languages, this does make a difference.
Summary
One of the powerful features of the ASP.NET Web API framework is binding, the process that creates the parameter objects for your action methods in the controller classes. Binding allows a developer to leave the repetitive work related to HTTP infrastructure to the framework and focus on implementing the business requirements at hand.
The ASP.NET Web API framework reads the route data and the query string of the request URI and sets the parameters of the action methods that are simple types (primitives, DateTime, Guid and so on) by the process called model binding. The out-of-the-box class that performs model binding is ModelBinderParameterBinding. The parameters that are complex types are set based on the HTTP request message body with the help of media type formatters by the process called formatter binding. The out-of-the-box class that performs formatter binding is FormatterParameterBinding. Both classes derive from an abstract class called HttpParameterBinding. By deriving our own custom class from HttpParameterBinding, we can create both simple and complex type parameters from any part of the HTTP request as well as entities outside of the request.
Media formatters have a role to play in both serialization and deserialization. A media formatter can enlist itself in serialization and/or deserialization by returning true when the framework calls the CanWriteType(Type) and CanReadType(Type) methods respectively.
There are various extensibility points available for us to hook our custom code into the binding process, such as creating a custom model binder by implementing IModelBinder, creating a custom value provider by implementing IValueProvider, creating a new media formatter by inheriting from MediaTypeFormatter, extending the capabilities of the out-of-the-box media formatters, creating a new parameter binder by inheriting from HttpParameterBinding, and so on.