CHAPTER 12

image

Media Type Formatters and Model Binding

As you learned in Chapter 3, one of the six REST constraints is the uniform interface constraint. Important parts of the uniform interface are representations of the URIs that identify our application entities or state. You also saw in Chapter 3 that the representation formats are identified by Internet media types (or MIME types, as they were called in the past). As ASP.NET Web API aims to ease the implementation of HTTP or even more RESTful applications, it obviously needs to provide a way to create and read from different Internet media types without a hassle. How this is done in ASP.NET Web API will be explained in the course of this chapter.

Overview

As ASP.NET Web API has been designed and developed with HTTP in mind, the result is an API that gives access to all elements of the HTTP specification in an elegant manner. Key parts of that specification for the ASP.NET Web API are Internet media types. They’re even more important in a RESTful application when the aim is to follow the uniform interface constraint. If you want to embrace HATEOAS on top of the Internet media types, as shown in Chapter 3, you definitely need a framework that provides an easy way to handle the creation and parsing of different types of content.

Besides the Internet media types whose content is being sent to the server as part of the request or response body (see Listing 12-1), HTTP also allows sending requests containing parameters in the URI, as Listing 12-2 shows.

Listing 12-1.  Response Body Containing a JSON Representation of a Contact

{
  "Name": "John Smith",
  "Age": 32,
  "Employed": true,
  "Address": {
    "Street": "701 First Ave.",
    "City": "Sunnyvale, CA 95125",
    "Country": "United States"
  }
}

Listing 12-2.  URI Containing Parameters

http://localhost/api/customers/?firstname=bill&lastname=gates&company=microsoft

ASP.NET Web API supports both writing and reading content from and to the request or response body, as well as binding URI parameters against a code entity. The first one is implemented in ASP.NET Web API by the formatter processing model, and the latter one is done by model binding, which is similar to the model binding mechanism being introduced by ASP.NET MVC. In the following sections of this chapter, you’ll see how the formatter processing model in ASP.NET Web API works and how you can implement your own formatters. After that you’ll learn how the model binding mechanism works and how both play together in some scenarios.

Formatter Processing Model

As you’ve already learned in Chapter 3, the client tells the server in which media type the response should be represented using the HTTP Accept header. If the server is able to serve a requested entity in the required representation format, it sends the response with a body containing the entity in that representation format. That process, part of the HTTP specification, is referred to as “content negotiation,” or just “conneg.”

Content Negotiation Algorithm

The basics of conneg are pretty simple, as described above, but the Accept header is not the only header being used. You may ask, “What happens if the server is not able to serve the media type requested by the client?” Let’s start with the headers that could be involved in conneg.

As we said, the HTTP header being used most is the Accept header. Please consider that our client is a processing tool for contact person images, and it can handle PNG besides other formats. So it wants to get a representation of a contact in the PNG image format—PNG supports transparency, which is useful for the integration of the processed contact image in web pages. The request using the Accept header would look like the one in Listing 12-3.

Listing 12-3.  Requesting a PNG Image Representation of a Contact

GET http://localhost/api/contact/1/ HTTP/1.1
Host: localhost
Accept: image/PNG

Another criterion the conneg could be based on is the language the client prefers the representation to be formatted in. For example, a list of product criteria should be retrieved in the language a human being is able to read, when the response of the request is displayed in its UI—it may be a web site. The header being used to request a specific language is the Accept-Language header, as shown in Listing 12-4.

Listing 12-4.  Requesting a List of Product Criteria in the German Language and JSON Format

GET http://localhost/api/product/1/criteria HTTP/1.1
Host: localhost
Accept: application/json
Accept-Language: de

Perhaps we are aware that the service from which we’re requesting the representation of the product criteria list might not be able to serve the list in German. In that case, we would want to be able to read it in English while still having German as the preferred language whenever possible. These scenarios are also part of the HTTP specification, and HTTP provides an extension for the Accept headers to be used in these cases. The extension provides a so-called quality/priority decimal factor ranging from 0 to 1. The 0 value represents the lowest quality; that is to say, we would avoid that Accept type or Language type if better ones are available. The value 1, on the other hand, represents the highest quality and is thus the most preferred. The quality/priority extension is abbreviated by the character q (for “quality”) followed by the decimal value depending on one’s preference. The complete usage mode of the Accept-Language header for the aforementioned product property scenario can be seen in Listing 12-5.

Listing 12-5.  Requesting Product Property, Preferably in German but Accepting English Also

GET http://localhost/api/product/1/criteria/ HTTP/1.1
Host: localhost
Accept: application/json
Accept-Language: de; q=1.0, en; q=0.5

Further Accept headers being considered for conneg are the Accept-Charset and the Accept-Encoding headers, as shown in Listings 12-6 and 12-7.

Listing 12-6.  Requesting UTF-8 Charset

GET http://localhost/api/product/1/criteria/ HTTP/1.1
Host: localhost
Accept: application/json
Accept-Charset: UTF-8

Listing 12-7.  Requesting Gzipped Content to Save Bandwith

GET http://localhost/api/product/1/criteria/ HTTP/1.1
Host: localhost
Accept: application/json
Accept-Encoding: application/x-gzip

As you see in these listings, the Accept headers can all be used in conjunction, and each one can also have its own quality/priority.

There is one scenario left which we haven’t yet covered. What happens if the client does not provide an Accept header? A valid option is to check whether the client sent a content-type header in the event that he did send content to the server within the request. If this is the case, the server simply tries to send the response containing the requested representation using the same Internet media type as the client sent within the request. Listing 12-8 shows the request, and Listing 12-9 the response.

Listing 12-8.  Request Without Accept Header but with Content-Type Header Set

POST http://localhost/api/products HTTP/1.1
Host: localhost
Content-Type: application/json

{
  "Name": "John Smith",
  "Age": 32,
  "Employed": true,
  "Address": {
    "Street": "701 First Ave.",
    "City": "Sunnyvale, CA 95125",
    "Country": "United States"
  }
}

Listing 12-9.  Response to Request Without Accept Header but with Content-Type Header Set

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{
  "Id": 1,
  "Name": "John Smith",
  "Age": 32,
  "Employed": true,
  "Address": {
    "Street": "701 First Ave.",
    "City": "Sunnyvale, CA 95125",
    "Country": "United States"
  }
}

Now that you’ve seen how content negotiation works at the HTTP level, we’re ready to take a look at how ASP.NET Web API allows us handle conneg at the .NET CLR level. As you have already learned in this chapter’s overview, ASP.NET Web API does this by providing a formatter processing model—but you haven’t yet learned what that means exactly. Now it’s time to look under the hood.

Media Type Formatters

ASP.NET Web API covers content negotiation, as described above, completely. Conneg mostly is about reading or creating representations of resources according to media type formats. In ASP.NET Web API, the basic functionality for that is encapsulated in the MediaTypeFormatter base class. During the next sections this class—and how to use it—will be described in detail.

ASP.NET Web API Content Negotiation in Practice

Before digging into the details of ASP.NET Web API conneg, let’s take a step back and recall what was learned in Chapter 9. We can simply return a CLR type from the .NET Framework or even a .NET object we’ve implemented ourselves, like the Car class one shown in Listing 12-10.

Listing 12-10.  A Car Class Implementation

public class Car {
    public int Id { get; set; }
    public string Make { get; set; }
    public string Model { get; set; }
    public int Year { get; set; }
    public float Price { get; set; }
}

In order to return an instance of this Car class, simply add a Get method to our CarController and return the instance being requested by its Id property, as shown in the simplified implementation of Listing 12-11.

Listing 12-11.  Returning a Car Entity Instance Using an ASP.NET Web API Controller

public class CarController : ApiController {
    public Car Get(int id) {
        return new Car() {
            Id = id,
            Make = "Porsche",
            Model = "911",
            Price = 100000,
            Year = 2012
        };
    }
}

If that controller action is invoked by navigating to the URI http://localhost:1051/api/car/1 using our web browser, the XML representation shown in Listing 12-12 is returned.

Listing 12-12.  XML Representation of the Cars Entity from Listing 12-11

<Car xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns="
http://schemas.datacontract.org/2004/07/WebApiConneg.Entities">
    <Id>1</Id>
    <Make>Porsche</Make>
    <Model>911</Model>
    <Price>100000</Price>
    <Year>2012</Year>
</Car>

Issuing a request, like the one in Listing 12-13, against the same URI using Fiddler but without specifying an Accept header produces a response returning a JSON representation, as Listing 12-14 confirms.

Listing 12-13.  Request to the Cars API Without Specifying an Accept Header

GET http://localhost:1051/api/car/1 HTTP/1.1
User-Agent: Fiddler
Host: localhost:1051

Listing 12-14.  Response Containing JSON Representation to the Cars API Request

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"Id":1,"Make":"Porsche","Model":"911","Year":2012,"Price":100000.0}

ASP.NET Web API Content Negotiation Demystified

What happened during the last two sample requests and responses? We didn’t handle media types in our controller implementation, nor did we add a configuration with which we told our Web API how to handle requests for specific media types or how to generate XML or JSON. As was said earlier, ASP.NET Web API introduces a processing model where the requested media types are parsed or created transparently to our code. That is, there’s no need to care about each Accept header we’ve learned about in the “Content Negotiation Algorithm” section to get the media type requested or to parse the content from the media type being specified in the content-type header in every request. By implementing parsing the data from or writing to the content once, it can be reused throughout the whole API (or even many projects).

In ASP.NET Web API, classes handling this parsing and writing are called formatters, and the abstract base class all formatters derive from is called MediaTypeFormatter. It resides in the System.Net.Http.Formatting namespace beside some other classes related to formatting. Some of them will be covered later in this chapter.

MediaTypeFormatter Class

The most important members of the MediaTypeFormatter class are shown in Listing 12-15.

Listing 12-15.  Members of the MediaTypeFormatter Base Class That Need to Be Overwritten or Set

public abstract class MediaTypeFormatter {
       public abstract bool CanReadType(Type type);
       public abstract bool CanWriteType(Type type);

       public virtual Task<object> ReadFromStreamAsync(Type type, Stream readStream,
HttpContent content, IFormatterLogger formatterLogger) {

              throw Error.NotSupported(Resources.MediaTypeFormatterCannotRead, new object[1] {
                    (object) this.GetType().Name
              });
       }

       public virtual Task WriteToStreamAsync(Type type, object value, Stream writeStream,
HttpContent content, TransportContext transportContext) {

              throw Error.NotSupported(Resources.MediaTypeFormatterCannotWrite, new object[1] {
                    (object) this.GetType().Name
              });
       }

       public Collection<Encoding> SupportedEncodings { get; private set; }
       public Collection<MediaTypeHeaderValue> SupportedMediaTypes { get; private set; }
}

In ASP.NET Web API, a formatter can always handle reading and writing within a given class. That’s why there are methods to read and write to the content stream: reading from the content stream is done by the ReadFromStreamAsync method, and writing to it is done using the WriteToStreamAsync method. There are also two methods that allow indicating whether our formatter is able to read (CanReadType method) or write (CanWriteType method) a type. The SupportedMediaTypes collection contains the media types the formatter is able to read and/or write. The collection contains the items type MediaTypeHeaderValue. The encodings supported by our formatter are listed in the SupportedEncodings collection, which contains items of the type Encoding.

You might ask where the MediaTypeFormatter instances get invoked. When an incoming request occurs, the controller to be invoked is set up. When this is done, the ApiControllerActionSelector tries to determine the type of the ParameterBindings for the selected controller action. This information is held in the HttpActionBinding property of the DefaultActionValueBinder. If a ParameterBinding should read its content from the requests body, a FormatterParameterBinding instance is created for that parameter. When every ParameterBinding is executed later on in the pipeline using the ExecuteBindingAsync method for each binding, the ReadAsAsync method of HttpContentExtensions is executed. This code calls the FindReader method of a newly instantiated MediaTypeFormatterCollection, which gets the collection of formatters being registered in the Web API configuration passed as a constructor parameter. The FindReader method itself queries all passed formatters and tries to find the best matching MediaTypeFormatter by evaluating the CanReadType method result and the supported Internet media type of each formatter in the collection.

If it finds a match, the formatter is returned to the HttpContentExtensions instance, and the content is read from the request body executing the ReadFromStreamAsync method of the formatter found in the previous step.

If no match is found, an exception is thrown; it indicates that no formatter can be found that is able to read the request body content with the specified media type.

If the formatter correctly deserializes the media type sent to our controller action, our controller action gets the created CLR type as a parameter and can continue processing the code being defined inside the controller method. See Listing 12-11 for an example.

To stick with the example from Listing 12-11, after the method is executed, the JSON representation of the car being created is returned to the client.

Creating this representation from the CLR Car class instance is also done using the JsonMediaTypeFormatter. This time, the WriteToStreamAsync method of the JsonMediaTypeFormatter class (more generally, a class deriving from the MediaTypeFormatter base class) is executed. As WriteToStreamAsync isn’t invoked by our own implementation inside the controller, there has to be automation similar to what we’ve seen for incoming requests. As you learned in Chapter 10, the last message handler for incoming requests in the Russian doll model is the HttpControllerDispatcher. As with all HttpMessageHandler implementations, the SendAsync method of the HttpControllerDispatcher is executed in the message handler chain (that is, the Russian doll model). Within the SendAsync method, the ExecuteAsync method of the controller (type ApiController) created using the HttpControllerDescriptor and the DefaultHttpControllerSelector is invoked.

From within this method, the InvokeActionAsync method of the ApiControllerActionInvoker is called. This in turn calls the Convert method of the ValueResultConverter, where the HttpRequestMessageExtensions.CreateResponse<T> method is invoked. This is the place where the Negotiate method of the DefaultContentNegotiator is called.

DefaultContentNegotiator: Finding a MediaTypeFormatter

The Negotiate method is the core of the conneg for response messages in ASP.NET Web API. The Negotiate method mainly uses two methods to do the conneg for a response message. The first one is the ComputeFormatterMatches method, as shown in Listing 12-16.

Listing 12-16.  ComputeFormatterMatches Method of the DefaultContentNegotiator

protected virtual Collection<MediaTypeFormatterMatch> ComputeFormatterMatches(
   Type type, HttpRequestMessage request, IEnumerable<MediaTypeFormatter> formatters) {
    
   if (type == (Type)null)
      throw Error.ArgumentNull("type");
   if (request == null)
      throw Error.ArgumentNull("request");
   if (formatters == null)
      throw Error.ArgumentNull("formatters");
    
   IEnumerable<MediaTypeWithQualityHeaderValue> sortedAcceptValues =
      (IEnumerable<MediaTypeWithQualityHeaderValue>)null;
    
   Collection<MediaTypeFormatterMatch> collection =
      new Collection<MediaTypeFormatterMatch>();
    
   foreach (MediaTypeFormatter formatter in formatters) {
      if (formatter.CanWriteType(type)) {
         MediaTypeFormatterMatch typeFormatterMatch1;
         if ((typeFormatterMatch1 =
            this.MatchMediaTypeMapping(request, formatter)) != null) {
            collection.Add(typeFormatterMatch1);
         }
         else {
            if (sortedAcceptValues == null)
               sortedAcceptValues =
                  this.SortMediaTypeWithQualityHeaderValuesByQFactor((
                  ICollection<MediaTypeWithQualityHeaderValue>)request.Headers.Accept);
            
            MediaTypeFormatterMatch typeFormatterMatch2;
            
            if ((typeFormatterMatch2 =
               this.MatchAcceptHeader(sortedAcceptValues, formatter)) != null) {
                  collection.Add(typeFormatterMatch2);
            }
            else {
               MediaTypeFormatterMatch typeFormatterMatch3;
               if ((typeFormatterMatch3 =
                  this.MatchRequestMediaType(request, formatter)) != null) {
                     collection.Add(typeFormatterMatch3);
               }
               else {
                  MediaTypeFormatterMatch typeFormatterMatch4;
                  if ((typeFormatterMatch4 =
                     this.MatchType(type, formatter)) != null)
                        collection.Add(typeFormatterMatch4);
               }
            }
         }
      }
   }
   return collection;
}

The ComputeFormatterMatches method determines how well each formatter being registered in the ASP.NET Web API configuration matches an HTTP request. This is done by a multilevel evaluation of the various Accept headers, including their quality factors. The collection of MediaTypeFormatterMatch created in the ComputeFormatterMatches is passed as a parameter to the SelectResponseMediaTypeFormatter method (see Listing 12-17).

Listing 12-17.  SelectResponseMediaTypeFormatter Method of DefaultContentNegotiator

protected virtual MediaTypeFormatterMatch
   SelectResponseMediaTypeFormatter(ICollection<MediaTypeFormatterMatch> matches) {
    if (matches == null)
      throw Error.ArgumentNull("matches");

   MediaTypeFormatterMatch typeFormatterMatch1 = (MediaTypeFormatterMatch)null;
   MediaTypeFormatterMatch typeFormatterMatch2 = (MediaTypeFormatterMatch)null;
   MediaTypeFormatterMatch typeFormatterMatch3 = (MediaTypeFormatterMatch)null;
   MediaTypeFormatterMatch typeFormatterMatch4 = (MediaTypeFormatterMatch)null;
   MediaTypeFormatterMatch current1 = (MediaTypeFormatterMatch)null;
   MediaTypeFormatterMatch typeFormatterMatch5 = (MediaTypeFormatterMatch)null;

   foreach (MediaTypeFormatterMatch potentialReplacement
      in (IEnumerable<MediaTypeFormatterMatch>)matches) {
      
      switch (potentialReplacement.Ranking) {
         case MediaTypeFormatterMatchRanking.MatchOnCanWriteType:
            if (typeFormatterMatch1 == null) {
                typeFormatterMatch1 = potentialReplacement;
               continue;
            }
            else
               continue;
         case MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderLiteral:
            typeFormatterMatch2 =
               this.UpdateBestMatch(typeFormatterMatch2,
                  potentialReplacement);
            continue;

         case MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderSubtypeMediaRange:
            typeFormatterMatch3 =
               this.UpdateBestMatch(typeFormatterMatch3,
                  potentialReplacement);
            continue;

         case MediaTypeFormatterMatchRanking.MatchOnRequestAcceptHeaderAllMediaRange:
            typeFormatterMatch4 =
               this.UpdateBestMatch(typeFormatterMatch4,
               potentialReplacement);
            continue;

         case MediaTypeFormatterMatchRanking.MatchOnRequestWithMediaTypeMapping:
            current1 = this.UpdateBestMatch(current1,
               potentialReplacement);
            continue;

         case MediaTypeFormatterMatchRanking.MatchOnRequestMediaType:
            if (typeFormatterMatch5 == null) {
               typeFormatterMatch5 = potentialReplacement;
               continue;
            }
            else
               continue;
         default:
            continue;
      }
   }

   if (current1 != null &&
      this.UpdateBestMatch(
         this.UpdateBestMatch(
            this.UpdateBestMatch(current1, typeFormatterMatch2),
            typeFormatterMatch3),
            typeFormatterMatch4) != current1)

      current1 = (MediaTypeFormatterMatch)null;

   MediaTypeFormatterMatch current2 = (MediaTypeFormatterMatch)null;

   if (current1 != null)
      current2 = current1;
   else if (typeFormatterMatch2 != null
         || typeFormatterMatch3 != null
         || typeFormatterMatch4 != null)
      current2 = this.UpdateBestMatch(
         this.UpdateBestMatch(
            this.UpdateBestMatch(current2, typeFormatterMatch2),
            typeFormatterMatch3),
            typeFormatterMatch4);
   else if (typeFormatterMatch5 != null)
      current2 = typeFormatterMatch5;
   else if (typeFormatterMatch1 != null)
      current2 = typeFormatterMatch1;
   return current2;
}

The SelectResponseMediaTypeFormatter method selects the correct media type (or the most reasonable match) for the response message by processing the MediaTypeFormatterMatch collection from the ComputeFormatterMatches method of Listing 12-16 and negotiating the collection of possibly matching formatters and their quality factors.

After the correct MediaTypeFormatter has been found, the SelectResponseCharacterEncoding selects the encoding for the response by evaluating the Accept-Encoding header and its quality factor (see Listing 12-18).

Listing 12-18.  SelectResponseCharacterEncoding Method of DefaultContentNegotiator

protected virtual Encoding SelectResponseCharacterEncoding(HttpRequestMessage request,
    MediaTypeFormatter formatter) {
        if (request == null)
            throw Error.ArgumentNull("request");
        if (formatter == null)
            throw Error.ArgumentNull("formatter");
        if (formatter.SupportedEncodings.Count <= 0)
            return (Encoding)null;
        
        foreach (StringWithQualityHeaderValue qualityHeaderValue in
            this.SortStringWithQualityHeaderValuesByQFactor(
                (ICollection<StringWithQualityHeaderValue>)request.Headers.AcceptCharset)) {
                    foreach (Encoding encoding in formatter.SupportedEncodings) {
                        if (encoding != null) {
                            double? quality = qualityHeaderValue.Quality;
                            if (
                                (quality.GetValueOrDefault() != 0.0
                                ? 1
                                : (!quality.HasValue ? 1 : 0)) != 0
                                && (qualityHeaderValue.Value.Equals(encoding.WebName,
                                    StringComparison.OrdinalIgnoreCase)
                                || qualityHeaderValue.Value.Equals("*",
                                    StringComparison.OrdinalIgnoreCase)))
                                        return encoding;
                        }
                    }
    }
    return formatter.SelectCharacterEncoding(
        request.Content != null ? request.Content.Headers : (HttpContentHeaders)null);
}

The SelectResponseCharacterEncoding method evaluates the incoming request’s Accept-Encoding header, iterates over the list of SupportedEncodings of the passed-in MediaTypeFormatter instance, and either selects the encoding with the highest quality factor or assigns the highest quality factor for the best matching one. Then it returns the best matching response encoding.

After that, the selected MediaTypeFormatter instance is returned to the HttpRequestMessageExtensions.CreateResponse<T> method, where a new HttpRequestMessage instance is created. The Content property of that instance gets assigned a new instance of type ObjectContent, which is written to the output stream using the assigned formatter farther along the response pipeline.

The last two sections of this chapter have shown how the HTTP content negotiation works in theory and in ASP.NET Web API. Now that you’ve seen what the intention of the MediaTypeFormatter base class is in ASP.NET Web API, let’s take a look at the MediaTypeFormatter implementations that are shipped with ASP.NET Web API by default.

Default Formatters

Over the years, the fact that a few media types in Web API development have proved to be available on many platforms has helped support interoperability. As it is likely that these media types might be requested by clients accessing Web APIs developed using ASP.NET Web API, a set of so-called default formatters is shipped with ASP.NET Web API, and so you don’t have to implement them by yourself. The next sections will examine these default formatters and show how to modify their behavior when it’s possible to do so.

JsonMediaTypeFormatter

The first MediaTypeFormatter implementation to discover is the JsonMediaTypeFormatter class. Residing in the System.Net.Http.Formatting namespace, it is able to read and write JSON body content from a request or to a response. Both operations are done using the Json.NET open source framework, which is a popular high-performance JSON framework for .NET.

During the early preview versions of ASP.NET Web API, Microsoft used its own DataContractJsonSerializer class, which was shipped as a part of the .NET Framework. As the DataContractJsonSerializer didn’t support serialization or deserialization of Ilist and similar types or case-insensitive property deserialization and was in addition pretty slow, the ASP.NET Web API team decided to drop the DataContractJsonSerializer in favor of the faster and more flexible Json.NET implementation. This is a novelty in the history of the .NET Framework, as it was the first time that Microsoft shipped a part of the .NET Framework that included an open source third-party library.

Besides the methods and properties derived from the MediaTypeFormatter base class, the JsonMediaTypeFormatter class provides several properties and another method:

  • Properties
  • SerializerSettings
  • UseDataContractJsonSerializer
  • Indent
  • MaxDepth
  • Method
  • CreateDefaultSerializerSettings

The JsonSerializerSettings class’s SerializerSettings property type allows you to modify the behavior of the JsonSerializer from the Json.NET framework. Since explaining all the possibilities of the JsonSerializer property and the JsonSerializerSettings class definitely goes beyond the scope of this chapter, we’ll cover a real-world scenario to show how the flexibility of the JSON serialization/deserialization process is introduced to the ASP.NET Web API by use of the Json.NET framework.

Suppose that your Web API implementation is consumed by a JavaScript client. It’s common to use camel case formatting for code written in JavaScript. As JSON is a sort of JavaScript code, you’d expect to use camel case for JSON also. By default, however, the JsonSerializer creates Pascal case–formatted JSON rather than camel case . To change that behavior, use the JsonSerializerSettings class.

The JsonSerializerSettings class provides a property, ContractResolver, that implements the IContractResolver interface. IContractResolver defines an interface for all its implementations, which are used by the JsonSerializer class to serialize/deserialize a .NET CLR type from or to JSON. Replacing the DefaultContractResolver implementation of IContractResolver can change the case style of the JSON returned by our JsonMediaTypeFormatter. Because camel case–formatted JSON occurs often with the Json.NET framework, Json.NET ships an implementation of a contract resolver that creates camel case JSON. That contract resolver is the CamelCasePropertyNamesContractResolver class, so we just need to create a new instance of JsonSerializerSettings and assign the camel case contract resolver instance to it, as shown in Listing 12-19.

Listing 12-19.  Changing the Casing of a JsonMediaTypeFormatter to Camel Case

var jsonFormatter = new JsonMediaTypeFormatter()  {
    SerializerSettings = new JsonSerializerSettings() {
        ContractResolver = new CamelCasePropertyNamesContractResolver()
    }
};

To learn more about using JsonSerializerSettings, we recommend that you read the Json.NET documentation (http://json.net).

Another property of the JsonMediaTypeFormatter class, UseDataContractJsonSerializer, a Boolean value, allows us to modify the behavior of the JSON media type formatter. The default value is false, but by setting it to true, the JsonMediaTypeFormatter can be forced to use the DataContractJsonSerializer, described at the beginning of this section.

The Indent property of the JsonMediaTypeFormatter class is another Boolean value; it allows us to create indented JSON, if needed, by setting the properties value to true (the default value is false).

The MaxDepth property of the JsonMediaTypeFormatter class allows us to define the level at which the JsonSerializer should stop deserializing nested child properties of the incoming JSON. If no value is specified, all nested child properties are deserialized.

The CreateDefaultSerializerSettings method of the JsonMediaTypeFormatter creates an instance of JsonSerializerSettings with the ContractResolver property set to the DefaultContractResolver, the MissingMemberHandling property set to MissingMemberHandling.Ignore, and the TypeNameHandling property set to TypeNameHandling.None. The created instance, used by the JsonMediaTypeFormatter itself, can also be used as a base for custom JsonSerializerSettings.

In the last section we saw how ASP.NET Web API allows us not only to create JSON out of the box but also to modify how JSON gets created using the default JsonMediaTypeFormatter and the underlying open source framework Json.NET.

XMLMediaTypeFormatter

Another important media type often used in Web APIs is XML. Like JSON, it is supported by default in ASP.NET Web API. XML in ASP.NET Web API is read and written by the XMLMediaTypeFormatter class, which resides in the same namespace as the JsonMediaTypeFormatter class.

The XMLMediaTypeFormatter by default uses the System.Runtime.Serialization.DataContractSerializer to serialize/deserialize XML in ASP.NET Web API. If you’re setting the UseXmlSerializer to true, a System.Xml.Serialization.XmlSerializer instance is used instead. You can also register a specific XmlSerializer or XmlObjectSerializer instance to read or write a specific CLR type. This is done using the SetSerializer<T> and SetSerializer methods and both their overloads.

The XMLMediaTypeFormatter class provides the Indent and MaxDepth properties, as the JsonMediaTypeFormatter class does. Their behaviors are the same.

FormUrlEncodedMediaTypeFormatter

Another scenario where you can use MediaTypeFormatters is in parsing the content of HTML forms submitted normally by a web browser. The media type of a submitted form is application/x-www-form-urlencoded; in ASP.NET Web API it is handled by the FormUrlEncodedMediaTypeFormatter class. A sample HTML form to create a new car is shown in Listing 12-20.

Listing 12-20.  A Sample HTML Form to Create a New Car

<form action="/api/cars" method="post">
<fieldset>
    <legend>New Car</legend>
    <label for="Make">Make:</label>
    <input type="text" name="Make" />
    <label for="Model">Model:</label>
    <input type="text" name="Model" />
    <label for="Year">Year:</label>
    <input type="text" name="Year" />
    <label for="Price">Price:</label>
    <input type="text" name="Price" />
    <input type="submit" />
</fieldset>
</form>

Figure 12-1 shows the filled HTML form.

9781430247258_Fig12-01.jpg

Figure 12-1 .  The filled HTML form to create a new car using ASP.NET Web API

The Post method of our CarsController, used to create a car from the form data, is shown in Listing 12-21.

Listing 12-21.  Post Method of the CarsController to Create a Car

public Car Post(FormDataCollection carFormData) {
    var carFormKeyValues = carFormData.ReadAsNameValueCollection();
    var car = new Car() {
        Make = carFormKeyValues["Make"],
        Model = carFormKeyValues["Model"],
        Price = Convert.ToSingle(carFormKeyValues["Price"]),
        Year = Convert.ToInt32(carFormKeyValues["Year"])
    };
    return car;
}

If the form is submitted by clicking the Submit button, the request issued can be traced using Fiddler, as shown in Listing 12-22.

Listing 12-22.  The Request of the Submitted HTML Form to Create a New Car Using ASP.NET Web API

POST http://localhost:1051/api/car HTTP/1.1
Host: localhost:1051
Connection: keep-alive
Content-Length: 45
Cache-Control: max-age=0
Origin: http://localhost:1051
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.11 (KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Referer: http://localhost:1051/htmlform.html
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3

Make=Porsche&Model=911&Year=2012&Price=100000

As Listing 12-22 shows, the form’s fields and values are added to the content body as a string, which is the x-www-form-urlencoded representation of our form.

Our request body content is parsed by the FormUrlEncodedMediaTypeFormatter; as the content can be parsed as a FormDataCollection class instance, the Post method of our CarController from Listing 12-21 gets invoked, and the FormDataCollection instance is passed in as a parameter. If our Post method is being debugged, the FormDataCollection instance is created by the FormUrlEncodedMediaTypeFormatter, as expected. The debugging result is shown in Figure 12-2.

9781430247258_Fig12-02.jpg

Figure 12-2 .  Debugging output of the carFormData parameter passed to the Post method of the CarController

Because the best matching Accept header value for the response created is application/xml;q=0.9, ASP.NET Web API then returns the new car representation as XML using the XMLMediaTypeFormatter, as the result in Listing 12-23 shows.

Listing 12-23.  Response for the Request Issued by Sending the HTML Form of Listing 12-20

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?YzpcdXNlcnNcYXplaXRsZXJcZG9jdW1lbnRzXHZpc3VhbCBzdHVkaW8gMjAxMlxQcm9qZWN0c1x
XZWJBcGlDb25uZWdcV2ViQXBpQ29ubmVnXGFwaVxjYXI=?=

X-Powered-By: ASP.NET
Date: Mon, 16 Jul 2012 19:01:52 GMT
Content-Length: 219

<Car xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns="
http://schemas.datacontract.org/2004/07/WebApiConneg.Entities"><Id>1</Id><Make>Porsche</Make>
<Model>911</Model><Price>100000</Price><Year>2012</Year></Car>

As you see now, by providing support for the application/x-www-form-urlencoded media type, ASP.NET Web API also allows us to have plain HTML clients to create new content for a Web API–based application.

JQueryMvcFormUrlEncodedFormatter

In the previous section, you saw how to post form data from an HTML form to a Web API controller. Due to the FormUrlEncodedMediaTypeFormatter and use of the FormDataCollection type as a parameter for the Post method of our controller (see Listing 12-21), this works nicely. Nevertheless, for at least two reasons the solution shown is not perfect. First, the signature of the Post method does not show which type we’re expecting to be passed in. Second, using the NameValueCollection returned by the ReadAsNameValueCollection method of the FormDataCollection instance is error-prone and not refactoring-safe. Furthermore, two Post methods might be needed if we want to allow HTML forms to post data to a Web API controller and also send XML or JSON to it.

A far more intuitive and refactoring-safe single-endpoint solution is shown in Listing 12-24.

Listing 12-24.  Post Method of the CarController to Create a Car

public Car Post(Car car) {
    if(null != car) {
        car.Id = 1;
        return car;
    }
    throw new HttpResponseException(new HttpResponseMessage() {
        StatusCode = HttpStatusCode.BadRequest,
        ReasonPhrase = "Car data must contain at least one value."
    });
}

If you submit the form from Listing 12-20 by clicking the Submit button again, the response shown in Listing 12-25 is returned.

Listing 12-25.  Response for the Request Issued by Sending the HTML Form of Listing 12-20

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?YzpcdXNlcnNcYXplaXRsZXJcZG9jdW1lbnRzXHZpc3VhbCBzdHVkaW8gMjAxMlxQcm9qZWN0c1x
XZWJBcGlDb25uZWdcV2ViQXBpQ29ubmVnXGFwaVxjYXI=?=

X-Powered-By: ASP.NET
Date: Mon, 16 Jul 2012 19:01:52 GMT
Content-Length: 219

<Car xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns="
http://schemas.datacontract.org/2004/07/WebApiConneg.Entities"><Id>1</Id><Make>Porsche</Make>
<Model>911</Model><Price>100000</Price><Year>2012</Year></Car>

This works without further parsing inside the controller’s action, because the request body content gets parsed by the JQueryMvcFormUrlEncodedFormatter. As the content can be parsed as a Car class instance, the Post method of our CarController from Listing 12-24 gets invoked, and the Car instance is passed in as a parameter. If we’re debugging our Post method, we can see that the Car instance has been created by the FormUrlEncodedMediaTypeFormatter, as expected. The debugging result is shown in Figure 12-3.

9781430247258_Fig12-03.jpg

Figure 12-3 .  Debugging Output of the Car Parameter Passed to the Post Method of the CarController

As you see now, by providing support for the application/x-www-form-urlencoded media type, ASP.NET Web API also allows plain HTML clients to create new safe content for a Web API–based application.

BufferedMediaTypeFormatter

When you implement a media type formatter derived from the abstract MediaTypeFormatter class, the serialization/deserialization inside the formatter should be handled by serializers supporting asynchronous serialization/deserialization of CLR objects. If this is not the case, ASP.NET Web API provides the BufferedMediaTypeFormatter base class, which provides a convenient way to read and write small, synchronous pieces of data. Listing 12-26 shows an implementation of a BufferedMediaTypeFormatter that supports reading and writing plain text.

Listing 12-26.  PlainTextMediaTypeFormatter to Handle a text/plain Media Type

public class PlainTextFormatter : BufferedMediaTypeFormatter {
    public PlainTextFormatter() {
        SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
        SupportedEncodings.Add(new UTF8Encoding());
        SupportedEncodings.Add(new UnicodeEncoding());
    }

    public override bool CanReadType(Type type) {
        return type == typeof(string);
    }

    public override bool CanWriteType(Type type) {
        return type == typeof(string);
    }

public class PlainTextFormatter : BufferedMediaTypeFormatter {
    public PlainTextFormatter() {
        SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/plain"));
        SupportedEncodings.Add(new UTF8Encoding());
        SupportedEncodings.Add(new UnicodeEncoding());
    }

    public override bool CanReadType(Type type) {
        return type == typeof(string);
    }

    public override bool CanWriteType(Type type) {
        return type == typeof(string);
    }

    public override object ReadFromStream(Type type,
        Stream stream,
        HttpContent content,
        IFormatterLogger formatterLogger) {
            Encoding selectedEncoding = SelectCharacterEncoding(content.Headers);
            using (var reader = new StreamReader(stream, selectedEncoding)) {
            return reader.ReadToEnd();
        }
    }

    public override void WriteToStream(Type type,
        object value,
        Stream stream,
        HttpContent content) {
            Encoding selectedEncoding = SelectCharacterEncoding(content.Headers);
            using (var writer = new StreamWriter(stream, selectedEncoding)) {
                writer.Write(value);
        }
    }
}
}

You can see in Listing 12-26 that there’s no call to the WriteToStreamAsync or ReadFromStreamAsync methods of the underlying MediaTypeFormatter base class, as these have been marked as sealed in the BufferedMediaTypeFormatter class we’re deriving from. Instead, the implementation overrides the synchronous ReadFromStream and WriteToStream methods and uses a synchronous StreamReader or StreamWriter class to read content from and write it to.

Custom Formatters

As already mentioned, ASP.NET Web API allows you to add custom formatter implementations to your Web API project either by deriving from the abstract MediaTypeFormatter base class or by deriving from one of the five default formatters shown in the preceding sections. That’s what we’re doing when we implement our first custom MediaTypeFormatter.

JsonpMediaTypeFormatter

When it comes to consuming a Web API with jQuery, you’ll want to use JSON; it’s lightweight and easy to handle with jQuery. This works pretty nicely until you try to receive JSON from a foreign domain. If that happens, you’ll face JavaScript browser security and won’t be able to retrieve the JSON data without working around the security constraint. The custom MediaTypeFormatter we’ll implement in this section will show you how to enable your public API to be consumed using JSON in a cross-domain scenario.

Let’s assume your domain is http://localhost:34847, and you want to get a JSON representation from http://localhost:40553/api/car/1 using the jQuery code shown in Listing 12-27.

Listing 12-27.  jQuery Cross-Domain Ajax Call

<script type="text/javascript">
    jQuery.getJSON("http://localhost:40553/api/car/1", function (car) {
        alert("Make: " + car.Make);
    });
</script>

If you run the code in Listing 12-27, you get back nothing—not even an error message. That’s because jQuery.getJSON is a wrapper for jQuery.ajax. Instead of using this wrapper, the complete code to create the XmlHttpRequest, using the wrapped jQuery.ajax method instead, looks like what’s shown in Listing 12-28.

Listing 12-28.  Complete jQuery Cross-Domain Ajax Call with Error Handling

<script type="text/javascript">
    jQuery.ajax({
        type: "GET",
        url: ' http://localhost:40553/car/1 ',
        dataType: "json",
        success: function (results) {
            alert("Success!");
        },
        error: function (XMLHttpRequest, textStatus, errorThrown) {
            alert("error");
        }
    });
</script>

When executing the code from Listing 12-28, you’ll get an alert with “error”. This happens because you’re trying to do cross-domain scripting, which is not allowed. To issue cross-domain jQuery Ajax calls receiving JSON representations, you have to use JSONP (that is, JSON with padding), which embeds a dynamically created <script> element containing the JSON representation into your document.

jQuery supports JSONP using jQuery.getJSON, as shown in Listing 12-29.

Listing 12-29.  Issuing a Cross-Domain Ajax Call Using jQuery and JSONP

<script type="text/javascript">
    jQuery.getJSON("http://localhost:40553/car/1/?callback =?", function (car) {
                  ("Make: " + car.Make);
       });
</script>

When it gets a URI like the one in Listing 12-29 as a parameter, jQuery replaces the last “?” with a dynamically created callback method name and embeds this method into the aforementioned script tag. Listing 12-30 shows a typical JSONP URI.

Listing 12-30.  A Typical JSONP URI Created by jQuery.getJSON

http://localhost:40553/car/1?callback=jsonp1311664395075

The name of the dynamically created callback function has to be returned by your Web API controller method on the server; if it matches the name of the one that jQuery sent to it, you’ll receive the JSON in your script for further handling. The JSONP response to the request looks like the one shown in Listing 12-31 (just the JSONP representation without the HTTP headers).

Listing 12-31.  JSONP Representation Without HTTP Headers

jsonp1311664395075({"Make":"BMW"})

The task that our custom MediaTypeFormatter has to solve is to prefix the JSON output with the JSONP method name passed in as a request parameter. Listing 12-32 shows the implementation of the JsonpMediaTypeFormatter.

Listing 12-32.  JsonpMediaTypeFormatter Implementation

using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web;

public class JsonpMediaTypeFormatter : JsonMediaTypeFormatter {
    private readonly HttpRequestMessage _request;
    private string _callbackQueryParameter;

    public JsonpMediaTypeFormatter() {
        SupportedMediaTypes.Add(DefaultMediaType);
        SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/javascript"));

        MediaTypeMappings.Add(new UriPathExtensionMapping("jsonp", DefaultMediaType));
    }

    public JsonpMediaTypeFormatter(HttpRequestMessage request) : this() {
        this._request = request;
    }

    public string CallbackQueryParameter {
        get { return _callbackQueryParameter ?? "callback"; }
        set { _callbackQueryParameter = value; }
    }

    public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type,
        HttpRequestMessage request,
        MediaTypeHeaderValue mediaType) {
            if (type == null)
                throw new ArgumentNullException("type");
            if (request == null)
                throw new ArgumentNullException("request");

            return new JsonpMediaTypeFormatter(request);
    }

    public override Task WriteToStreamAsync(Type type,
        object value,
        Stream stream,
        HttpContent content,
        TransportContext transportContext) {
            string callback;
            if (IsJsonpRequest(_request, out callback)) {
                return Task.Factory.StartNew(() => {
                    var writer = new StreamWriter(stream);
                    writer.Write(callback + "(");
                    writer.Flush();

                    base.WriteToStreamAsync(type,
                        value, stream, content, transportContext)
                            .ContinueWith(_ => {
                                writer.Write(")");
                                writer.Flush();
                    });
                });
            }

        return base.WriteToStreamAsync(type,
            value, stream, content, transportContext);
    }

    private bool IsJsonpRequest(HttpRequestMessage request,
        out string callback) {
            callback = null;

            if (request == null || request.Method != HttpMethod.Get) {
                return false;
            }

            var query =
                HttpUtility.ParseQueryString(request.RequestUri.Query);
            
            callback = query[CallbackQueryParameter];

            return !string.IsNullOrEmpty(callback);
    }
}

The most relevant part of the code in Listing 12-32 is the WriteToStreamAsync method, which first checks whether the incoming request is a JSONP request. The incoming request is a JSONP request if it contains the URI parameter “callback”. The name of that parameter is stored in the public property CallbackQueryParameter, which allows us to change this name when instantiating the formatter. If the request is a JSONP request, the already created JSON response content is written to a string and surrounded with the jQuery callback method name and opening and closing braces, so that it matches the format shown in Listing 12-31.

One thing not mentioned yet is a behavior specific to JSONP requests: they have an Accept header value of */*, which would break the conneg mechanism in general and Web API specifically, because the conneg would not be able to negotiate a valid response media type. The workaround for that issue is to have a distinct URI that returns only JSONP. So instead of using the URI in Listing 12-30, we need to provide a Web API URI like the one in Listing 12-33.

Listing 12-33.  A Distinct URI Providing JSONP

http://localhost:40553/car/1/jsonp?callback=jsonp1311664395075

The problem now is how to tell the formatter that URIs containing the jsonp URI fragment should be treated as text/javascript media type. This is done by using a MediaTypeMapping property, which tells the Web API to set an Accept header value of text/javascript if the URI for an incoming request has the same jsonp fragment as the last one. That MediaTypeMapping is added in the last line of our JsonpMediaTypeFormatter’s constructor. Media type mappings are discussed in more detail later in this chapter.

Another task to complete is updating our default route definition in our Web API configuration. The URI shown in Listing 12-33 would generate a 404 error response, as the default URI template does not contain a definition for the jsonp fragment. To solve the problem, add another optional RouteParameter called format—or you can choose any other distinct name (see Listing 12-34).

Listing 12-34.  Enabling the Media Type Mapping Fragment in Web API Routing

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

If we now reissue our cross-domain jQuery Ajax call again, we get the data back from the Web API, as you can see in Figure 12-4.

9781430247258_Fig12-04.jpg

Figure 12-4 .  JSONP result displayed for a cross-domain jQuery request

CSVMediaTypeFormatter

Another data exchange format, one still heavily used in the software industry, is CSV, an acronym for “comma-separated values.” A sample CSV file’s content—it lists some cars, to stick with our model—is shown in Listing 12-35.

Listing 12-35.  Sample CSV File Content Listing Some Cars

17,VW, Golf,1999,1500
24,Porsche,911,2011,80000
30,Mercedes,A-Class,2007,10000

As Listing 12-35 shows, CSV has no column headers, and the order of the columns matters. Now let’s consider a car reseller that needs a list of cars in the CSV format in order to import this file into Excel and do some further data processing. The described scenario can be easily expressed in ASP.NET Web API by adding a CarCsvMediaTypeFormatter class (this is a write-only media type formatter). As we’re using the synchronous StreamWriter class to serialize the Car class to CSV format, the derivation is from the BufferedMediaTypeFormatter class. The implementation is shown in Listing 12-36.

Listing 12-36.  A CSV Media Type Formatter Implementation

public class CarCsvMediaTypeFormatter : BufferedMediaTypeFormatter {
        
    static readonly char[] SpecialChars = new char[] { ',', ' ', ' ', '"' };

    public CarCsvMediaTypeFormatter() {
        SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/csv"));
    }

    public override bool CanReadType(Type type) {
        return false;
    }

    public override bool CanWriteType(Type type) {
        if (type == typeof(Car)) {
            return true;
        }
        else {
            var enumerableType = typeof(IEnumerable<Car>);
            return enumerableType.IsAssignableFrom(type);
        }
    }

    public override void WriteToStream(Type type,
        object value,
        Stream stream,
        HttpContent content) {
            using (var writer = new StreamWriter(stream)) {

                var cars = value as IEnumerable<Car>;
                if (cars != null) {
                    foreach (var car in cars) {
                        writeItem(car, writer);
                    }
                }
                else {
                    var car = value as Car;
                    if (car == null) {
                        throw new InvalidOperationException("Cannot serialize type");
                    }
                    writeItem(car, writer);
                }
        }
        stream.Close();
    }

    private void writeItem(Car car, StreamWriter writer) {
        writer.WriteLine("{0},{1},{2},{3},{4}", Escape(car.Id),
            Escape(car.Make), Escape(car.Make), Escape(car.Year), Escape(car.Price));
    }

    private string Escape(object o) {
        if (o == null) {
            return "";
        }
        var field = o.ToString();
        return field.IndexOfAny(SpecialChars) !=1
            ? String.Format(""{0}"", field.Replace(""", """")) : field;
    }
}

The most relevant work in the CarCsvMediaTypeFormatter happens in the WriteToStream method, where the formatter first checks whether to serialize a collection of cars or a single car item and then starts serializing the corresponding instance. In the writeItem method the serialization of a single car item happens. The CSV format gets broken when a character like a comma or a line break is used inside the values. To avoid this, values containing commas or line breaks are eliminated using the Escape method before being serialized.

When invoking the request to the Get method of the CarsController (see Listing 12-37) and setting the Accept header to the text/csv media type, the response gotten is shown in Listing 12-38.

Listing 12-37.  CarsController Implementation

public class CarsController : ApiController {
    public List<Car> Get() {
        return new List<Car>() {
                new Car() {
                        Id = 17,
                        Make = "VW",
                        Model = "Golf",
                        Year = 1999,
                        Price = 1500f
                    },
                new Car() {
                        Id = 24,
                        Make = "Porsche",
                        Model = "911",
                        Year = 2011,
                        Price = 100000f
                    },
                new Car() {
                        Id = 30,
                        Make = "Mercedes",
                        Model = "A-Class",
                        Year = 2007,
                        Price = 10000f
                    }
            };
    }

Listing 12-38.  CSV Formatted Response

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: text/csv
Expires: -1
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RDpcRHJvcGJveFxQRE1MYWJcUHJvamVrdGVcMTAwMDU3IC0gV2ViIEFQSSBCdWNoXFByb1dlYkF
QSVxDYXJDc3ZNZWRpYVR5cGVGb3JtYXR0ZXJcQ2FyQ3N2TWVkaWFUeXBlRm9ybWF0dGVyXGFwaVxjYXJz?=

X-Powered-By: ASP.NET
Date: Tue, 17 Jul 2012 09:48:03 GMT
Content-Length: 82

17,VW,Golf,1999,1500
24,Porsche,911,2011,100000
30,Mercedes,A-Class,2007,10000

You’ve seen how formatters work and how to implement custom formatters, but there are still some things missing from the explanation: ASP.NET Web API needs to be told to use your formatter implementations at runtime—unless existing formatters are removed. That’s what you’ll discover in the next sections of this chapter.

Formatter Configuration

As for other extensibility points, like message handlers, ASP.NET Web API also offers the possibility to add, change, remove, or even reorder the default or custom formatters. Let’s start with the modification of an existing formatter, one we’ve seen during this chapter’s last few sections.

Modifying Existing Formatters

Implementing a new formatter for ASP.NET Web API is one option for modifying or extending the conneg behavior of ASP.NET Web API. Another option is modifying existing formatters in order to meet your needs. A good example of modifying existing formatters by configuration is the JsonMediaTypeFormatter, from this chapter’s “Default Formatters” section, where we changed the SerializerSettings to output camel case JSON instead of Pascal case JSON. Back in that section we created a new instance of the default JsonMediaTypeFormatter using modified SerializerSettings, but we did not assign it to our Web API configuration. Instead of creating a new instance, we can modify the configuration of the already registered default JsonMediaTypeFormatter.

Registration of formatters occurs in the case of web hosting in the Application_Start method of the WebApiApplication class, which resides in the Global.asax.cs file. To modify the SerializerSettings of the default JsonMediaTypeFormatter, use the code shown in Listing 12-39.

Listing 12-39.  Modifying the Behavior of the Default JsonMediaTypeFormatter

public class Global : HttpApplication {
    protected void Application_Start(object sender, EventArgs e) {
        var config = GlobalConfiguration.Configuration;
        config.Formatters.JsonFormatter.SerializerSettings =
            new JsonSerializerSettings() {
                ContractResolver = new CamelCasePropertyNamesContractResolver()
        };
        RouteConfig.RegisterRoutes(RouteTable.Routes);
    }
}

Issuing a new request to our Web API after that modification and requesting application/json as media type produces the result shown in Listing 12-40.

Listing 12-40.  Camel Case JSON (Without HTTP Response Headers)

{"id":1,"make":"Porsche","model":"911","year":2012,"price":100000.0}

Registering a New Formatter

Implementing a new formatter for ASP.NET Web API is the first part of the work that has do be done to support Internet media beyond the defaults supported out of the box. The second part is to register the new formatter implementations in ASP.NET Web API configuration; this is done to tell ASP.NET Web API that there are more media types that it can handle.

As you saw in the last section, the default formatters are registered in the GlobalConfiguration.Configuration class. The same goes for custom formatters. Listing 12-41 shows how to register our implementation of a PlainTextFormatter from earlier in this chapter.

Listing 12-41.  Registering a New Formatter in ASP.NET Web API Configuration

public class Global : HttpApplication {
    protected void Application_Start(object sender, EventArgs e) {
        var config = GlobalConfiguration.Configuration;
        config.Formatters.Add(new PlainTextFormatter());
        RouteConfig.RegisterRoutes(RouteTable.Routes);
    }
}

Listing 12-41 uses the Add method of the MediaTypeFormatterCollection to add a new MediaTypeFormatter implementation. The Add method allows us to add a new MediaTypeFormatter instance at the end of the formatter list. In contrast, the Insert method of the MediaTypeFormatterCollection adds a new MediaTypeFormatter instance at a specific index of the list (see Listing 12-42).

Listing 12-42.  Adding a New Formatter at a Specific Position of the MediaTypeFormatterCollection

public class Global : HttpApplication {
    protected void Application_Start(object sender, EventArgs e) {
        var config = GlobalConfiguration.Configuration;
        config.Formatters.Insert(0, new JsonpMediaTypeFormatter());
        RouteConfig.RegisterRoutes(RouteTable.Routes);
    }
}

Removing a Formatter

In Listing 12-42 we added a new formatter at a specific index of our ASP.NET Web API media type formatter configuration. As was mentioned earlier, ASP.NET Web API has some default formatters, which are configured when you create a new ASP.NET Web API application. One of these, the JsonMediaTypeFormatter, is by default the formatter at index 0. We registered our JsonpMediaTypeFormatter in our configuration, and so we now have two formatters able to read and write JSON. One being enough for now, the default, JsonMediaTypeFormatter, can safely be removed, as we want to be able to create JSONP as well as JSON. Listing 12-43 shows updated code from Listing 12-42, including removal of the default JsonMediaTypeFormatter.

Listing 12-43.  Replacing the Default JsonMediaTypeFormatter

public class Global : HttpApplication {
    protected void Application_Start(object sender, EventArgs e) {
        var config = GlobalConfiguration.Configuration;
        config.Formatters.Remove(config.Formatters.JsonFormatter);
        config.Formatters.Insert(0, new JsonpMediaTypeFormatter());
        RouteConfig.RegisterRoutes(RouteTable.Routes);
    }
}

Changing the Formatter Order

In this chapter’s two previous sections, we inserted a new MediaTypeFormatter and removed an existing one. Instead of inserting the JsonpMediaTypeFormatter as we did there, we could have added it via the Add method, as shown before. If we had done this and issued a JSONP request to our Web API application, we would have gotten a script error during the execution of the JSONP request, because the request gets handled by the first MediaTypeFormatter able to handle a specific media type. In the case of the default ASP.NET Web API configuration this is the JsonMediaTypeFormatter. But for the JsonpMediaTypeFormatter to work, it needs to be registered before the JsonMediaTypeFormatter. This explains why inserting the JsonpMediaTypeFormatter at index 0 worked but adding it at the end of the list of formatters did not. So what we learn from this simple sample is that the registration order of MediaTypeFormatter instances matters. Keep that in mind when registering new MediaTypeFormatters!

As the last four sections have shown, modifying, adding, and removing MediaTypeFormatter instances in ASP.NET Web API is straightforward and quite simple. The only thing you must be aware of is the correct order of the formatter registration to avoid unwanted effects and get the correct MediaTypeFormatter instance invoked.

Media Type Mappings

In the earlier “JsonpMediaTypeFormatter” section, you saw that ASP.NET Web API offers a way to reroute a request to a specific formatter by mapping an incoming media type header to another media type. This technique, which ASP.NET Web API provides, is called MediaTypeMappings; we’ll take a closer look at these mappings in the following sections.

Involving Content Negotiation with MediaTypeMapping

Every MediaTypeFormatter implementation has a property, MediaTypeMappings, which is a type of Collection<MediaTypeMapping>. MediaTypeMapping provides a way for developers to add some custom logic and decide whether they want the formatter to take part in writing the response. This differs from the default way of matching a media type value, like application/json, based on the request Accept and content-type headers and doing the content negotiation on the basis of that information.

Default Media Type Mappings

In the “JsonpMediaTypeFormatter” section we created a UriPathExtensionMapping instance and added it to the list of media type mappings of the JsonpMediaFormatter implementation. The UriPathExtensionMapping class is only one of three default MediaTypeMapping types being shipped with ASP.NET Web API by default. The three MediaTypeMapping types in ASP.NET Web API are

  • UriPathExtensionMapping
  • QueryStringMapping
  • RequestHeaderMapping

In order to get an understanding of which MediaTypeMapping to use and how and when to use it, we’ll take a closer look at each now.

UriPathExtensionMapping

The “JsonpMediaTypeFormatter” section already showed a UriPathExtensionMapping inside our JsonpMediaTypeFormatter constructor implementation. The UriPathExtensionMapping allows us to map a URI ending with a specific fragment to an arbitrary media type. This allows us to set the requested media type for the response using the URI instead of the Accept header. Again, as already shown in the “JsonpMediaTypeFormatter” section, this is useful for jQuery JSONP requests, as well as for scenarios where the client is a browser. As the user cannot modify the Accept header using the browser, providing a link with the media type in the URI enables the browser to request a specific media type.

QueryStringMapping

A QueryStringMapping can be used to add a media type based on a QueryString parameter. (We used a QueryString parameter in the “JsonpMediaTypeFormatter” section earlier when passing the jQuery method name to our JsonpMediaTypeFormatter instance.) The jQuery QueryString parameter is highlighted in Listing 12-44.

Listing 12-44.  QueryString Parameter in jQuery Cross-Domain Ajax Call

http://localhost:40553/car/1?callback=jsonp1311664395075

As Listing 12-44 shows, the name of the QueryString parameter is callback, and its value is jsonp1311664395075. In the JsonpMediaTypeFormatter implementation, we’ve added a UriPathExtensionMapping to the URI fragment jsonp and to the application/json media type. Using a QueryStringMapping would have changed our URI format to look like Listing 12-45 for JSONP requests.

Listing 12-45.  URI Using a QueryString Parameter to Define the Requested Media Type

http://localhost:40553/car/1?format=jsonp&callback=jsonp1311664395075

In order to get that URI working, we need to register the media type mapping in the JsonpMediaTypeFormatter constructor, as shown in Listing 12-46.

Listing 12-46.  Registering a QueryStringMapping Instead of a UriPathExtensionMapping

public JsonpMediaTypeFormatter() {    SupportedMediaTypes.Add(DefaultMediaType);
    SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/javascript"));
    MediaTypeMappings.Add(new QueryStringMapping("format", "jsonp", DefaultMediaType));
}

When running the cross-domain jQuery call using the URI from Listing 12-43 now, we get a return request for our JSONP representation of the Car instance, as expected.

RequestHeaderMapping

Another media type mapping that ASP.NET Web API provides to modify the conneg process is the RequestHeaderMapping class. As the name suggests, it allows us to change the media type being requested from a formatter by evaluating an arbitrary request header. For example, the default formatter is the JsonpMediaTypeFormatter; if the request comes from the same web site as the Web API is hosted on, it should forward the request to the text/xml media type in order to return XML instead of JSON. If the request is a cross-domain JSONP request, JSONP should be treated in the way shown before. This can be accomplished by using the configuration code in Listing 12-47.

Listing 12-47.  Mapping the Media Type Based on the Referer Header Value Using ASP.NET Web API Configuration

public class Global : HttpApplication {
    protected void Application_Start(object sender, EventArgs e)     {
        var config = GlobalConfiguration.Configuration;
        config.Formatters.Remove(config.Formatters.JsonFormatter);
        config.Formatters.Insert(0, new JsonpMediaTypeFormatter());
        
        config.Formatters.JsonFormatter.MediaTypeMappings.Add(
            new RequestHeaderMapping(
                "Referer",
                " http://localhost:1501/",
                StringComparison.InvariantCultureIgnoreCase,
                false,
                "text/xml"));
                
        RouteConfig.RegisterRoutes(RouteTable.Routes);
    }
}

In contrast to the first two media type mapping types, the RequestHeaderMapping has not been added inside the MediaTypeFormatter implementation; instead, it has been applied at configuration level. This is also a valid option and allows us to add mappings without needing to modify a MediaTypeFormatter. The decision of which mapping (if any) to use can easily change from application to application.

As with other features discussed in this chapter, ASP.NET Web API allows us to customize the behavior in terms of media type mappings by implementing custom MediaTypeMapping classes—which are covered in the next section.

A Custom Media Type Mapping: RouteDataMapping

There may be scenarios where the aforementioned default MediaTypeMapping implementations might not suit your needs—as, for instance, when you want to include the mapping as part of your route definition. You’ll want to provide URIs like the one shown in Listing 12-48.

Listing 12-48.  URI Containing the Media Type Defined by a Route Template

http://localhost:1051/api/cars.json

The proper URI template for the URI shown in Listing 12-48 is the route definition shown in Listing 12-49.

Listing 12-49.  Route Definition for URIs Containing the Media Type Selection

GlobalConfiguration.Configuration.Routes.MapHttpRoute(
    "defaultHttpRoute",
    routeTemplate: "api/{controller}.{extension}",
    defaults: new { },
    constraints: new { extension = "json|xml" }
);

The implementation of the RouteDataMapping looks like what is shown in Listing 12-50.

Listing 12-50.  RouteDataMapping Implementation

public class RouteDataMapping : MediaTypeMapping {

    private readonly string _routeDataValueName;
    private readonly string _routeDataValueValue;

    public RouteDataMapping(
        string routeDataValueName,
        string routeDataValueValue,
        MediaTypeHeaderValue mediaType) : base(mediaType) {
            _routeDataValueName = routeDataValueName;
            _routeDataValueValue = routeDataValueValue;
    }

    public override double TryMatchMediaType(HttpRequestMessage request) {
        return (
            request.GetRouteData().
                Values[_routeDataValueName].ToString() == _routeDataValueValue)
                    ? 1.0 : 0.0;
    }
}

The name of the URI template parameter—it is extension here—and the value for the extension, as well as the matching media type, are passed as constructor parameters. At runtime, the TryMatchMediaType method evaluates whether the media type matches by comparing the extension value of the incoming request. If both are a match, the return value for the quality factor is 1.0 otherwise it is 0.0, which means it is not a match.

To get the RouteDataMapping working, we need to assign it to our JsonMediaTypeFormatter and the XMLMediaTypeFormatter in the Web API configuration (see Listing 12-51).

Listing 12-51.  Applying the RouteDataMapping to the Default XML and JSON Formatters

protected void Application_Start(object sender, EventArgs e) {
    var config = GlobalConfiguration.Configuration;
    
    GlobalConfiguration.Configuration.Formatters.JsonFormatter.
        MediaTypeMappings.Add(
            new RouteDataMapping(
                "extension",
                "json",
                new MediaTypeHeaderValue("application/json")));

    GlobalConfiguration.Configuration.Formatters.XmlFormatter.
        MediaTypeMappings.Add(
            new RouteDataMapping(
                "extension",
                "xml",
                new MediaTypeHeaderValue("application/xml")));

    RouteConfig.RegisterRoutes(RouteTable.Routes);

As Listing 12-51 shows, the .json extension is mapped to the JsonFormatter, and the .xml extension is mapped to the XmlFormatter, with the appropriate media type assigned in each case.

image Tip  In order to avoid 404 errors when using this solution, you need to set <modules runAllManagedModulesForAllRequests="true" /> in the <system.webServer> section in web.config.

When calling the URI http://localhost:1051/api/cars.json in our browser, we get the expected list of cars as a JSON representation, whereas the URI http://localhost:1051/api/cars.xml returns the list of cars as an XML document, as shown in Listings 12-52 and 12-53.

Listing 12-52.  JSON Result forhttp://localhost:1051/api/cars.json

[{"Id":17,"Make":"VW","Model":"Golf","Year":1999,"Price":1500.0},
{"Id":24,"Make":"Porsche","Model":"911","Year":2011,"Price":100000.0},
{"Id":30,"Make":"Mercedes","Model":"A-Class","Year":2007,"Price":10000.0}]

Listing 12-53.  XML Result forhttp://localhost:1051/api/cars.xml

<ArrayOfCar xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://schemas.datacontract.org/2004/07/WebApiConneg.Entities"><Car><Id>17</Id>
<Make>VW</Make><Model>Golf</Model><Price>1500</Price><Year>1999</Year></Car><Car><Id>24</Id>
<Make>Porsche</Make><Model>911</Model><Price>100000</Price><Year>2011</Year></Car><Car><Id>30</Id>
<Make>Mercedes</Make><Model>A-Class</Model><Price>10000</Price><Year>2007</Year></Car></ArrayOfCar>

To this point in the chapter, we have explained HTTP content negotiation in general and ASP.NET Web API conneg in particular. As you’ve learned, the conneg part is specific to request and response body content and is handled by MediaTypeFormatter instances and enhanced using MediaTypeMappings. As you also saw at the beginning of the chapter, ASP.NET Web API allows us to create ApiController method parameters not only from the request body but also from the URI. This part is covered by the model binding mechanism in ASP.NET Web API; we’ll see what this means in the sections that follow.

Model Binding

ASP.NET Web API not only supports reading content from the request body or writing it to the response body; it also allows us to create simple types using QueryString parameters from incoming requests. This is possible for simple types like System.String or System.Int32, as well as for simple custom classes, like the Car class implementation used earlier in this chapter.

This process of mapping QueryString parameters to parameters for your controller’s action methods is called model binding; it was introduced with ASP.NET MVC a few years ago. Model binding keeps your controller code clean from parsing QueryString parameters and allows you to do it in a generic, reusable way.

Model Binder Mechanism

Besides using body content like JSON or XML in requests (as seen in recent sections), we can also issue a request having QueryString parameters only (if it is a GET or DELETE request) or a content body and QueryString parameters (if we have a POST or PUT request). A sample GET request URI having two QueryString parameters is shown in Listing 12-54.

Listing 12-54.  GET Request URI Having Two QueryString Parameters

http://localhost:1051/api/values/?param1=1& param2=2

In Listing 12-54, the QueryString parameters are param1, having a value of 1 and param2, having a value of 2.

Without the model binding mechanism, your controller might look like Listing 12-55.

Listing 12-55.  Manually Parsing QueryString Parameter Inside a Web API Controller

public class ValuesController : ApiController {
    public int Get() {
        var param1 = Request.RequestUri.ParseQueryString().Get("param1");
        var param2 = Request.RequestUri.ParseQueryString().Get("param2");

        if (!string.IsNullOrEmpty(param1) && !String.IsNullOrEmpty(param2)) {
            return Convert.ToInt32(param1) + Convert.ToInt32(param2);
        }

        throw new HttpResponseException(HttpStatusCode.BadRequest);
    }
}

The goal of the ValuesController in Listing 12-55 is simply to sum the values of the two incoming parameters and return the result. Because we have to parse the values (we aren’t even validating them) from the params by ourselves, the code is bloated and hard to read—and we have only two numbers here!

As we said earlier, model binding was introduced with ASP.NET MVC a few years ago. It has been adopted in ASP.NET Web API for handling everything in a request save for media types (the request’s body content).

The basic idea of model binding is to retrieve simple data (“simple” in the sense of structure), such as QueryString parameters and headers, from the request using Value Providers and then compose the data into a model using model binders.

This makes handling QueryString parameters much easier, as you can see in Listing 12-56. The ValuesController from the previous listing has now been reduced to the main goal: sum two incoming parameters and return that result.

Listing 12-56.  An API Controller Accessing QueryString Parameters

public class ValuesController : ApiController {
    public int Get(int param1, int param2) {
        return param1 + param2;
    }
}

If you debug that request (see Figure 12-5), you will see that both parameters are assigned to our Get method as parameters.

9781430247258_Fig12-05.jpg

Figure 12-5 .  QueryString parameters being assigned to a controller method

Listing 12-56 works, because the model binding does the mapping between the QueryString parameters and the controller’s action method parameters for us.

As mentioned earlier, ASP.NET Web API lets us bind QueryString parameters not only against simple CLR types but also to simple CLR objects. Listing 12-57 shows a Search class implementation having two properties to define a simple search and text to search for and the maximum number of results to be returned.

Listing 12-57.  Search Class Implementation

public class Search {
    public string Text { get; set; }
    public int MaxResults { get; set; }
}

The Search class is used as a parameter for the SearchController shown in Listing 12-58.

Listing 12-58.  SearchController to Perform a Search for Persons’ Names

public class SearchController : ApiController {
    private readonly string[] __persons =
        new[] { "Bill", "Steve", "Scott", "Glenn", "Daniel" };

    public IEnumerable<string> Get([FromUri] Search search) {
        return _persons
        .Where(w => w.Contains(search.Text))
        .Take(search.MaxResults);
    }
}

To perform a search using the controller, browse the URI shown in Listing 12-59.

Listing 12-59.  Performing a Search for Persons Based on QueryString Parameters

http://localhost:1051/api/search/?text=Bill&maxresults=2

If browsing the URI in Listing 12-59 is done with a web browser, we get back an XML document containing Bill as a search result.

In Listing 12-58, as you see, we aid the model binding by telling the controller that the Search parameter should be bound against parameters from the URI by assigning the [FromUri] attribute to the Search parameter type. There’s also a [FromBody] attribute that is used by default, but you can also specify it explicitly; you can also mix both [FromUri] and [FromBody] within a single method signature.

In the introduction of this section we said that you can also mix body content and QueryString parameters; that’s what we’ll try in the next example.

In this example we’ll use our Car class again; we’ll perform a PUT operation where we explicitly pass the id of the Car resource to be updated as a QueryString parameter. The controller implementation is shown in Listing 12-60.

Listing 12-60.  Mixing QueryString Parameters and Request Body Content

public class CarController : ApiController {
    public Car Put([FromUri]int id, Car car) {
        return car;
    }
}

Now let’s issue a request like the one in Listing 12-61. When debugging this request, you can see that both id and car parameters are bound against the correct request QueryString and body content parameter, as seen in Figure 12-6.

Listing 12-61.  A PUT Request Containing QueryString and Body Content Parameters

PUT http://localhost:1051/api/car?&id=1 HTTP/1.1
User-Agent: Fiddler
Host: localhost:1051
Content-Length: 70
Content-Type: application/json

{ "Id": 1, "Make":"VW", "Model":"Golf II", "Year":1995, "Price":1000 }

9781430247258_Fig12-06.jpg

Figure 12-6 .  Debugging parameters from QueryString and request body content

Now that you’ve seen the ASP.NET Web API model binding in action, you might wonder how this works under the hood. That’s the subject of the last section.

Default Model Binder

As we learned in this chapter’s “MediaTypeFormatter Class” section, when the controller to be invoked is set up, the type of the ParameterBindings is determined. This is done by the ApiControllerActionSelector, by querying the HttpActionBinding from the DefaultActionValueBinder. If a ParameterBinding should read its content from the requests body, a FormatterParameterBinding instance is created for that parameter.

On the other hand, when a ParameterBinding reads the content from a QueryString parameter, a ModelBinderParameterBinding instance is created for that parameter. This binding is executed by invoking the ExecuteBindingAsync method of the binding in the same way FormatterParameterBinding is. This is so because it has the same HttpParameterBinding base class.

Inside the ExecuteBindingAsync method, the GetValueProvider method of the CompositeValueProviderFactory is executed. This method tries to collect all IValueProvider instances from all ValueProviderFactory instances registered in the DefaultServices class for ASP.NET Web API. In the case of the QueryStringValueProviderFactory, this is the QueryStringValueProvider that itself derives from the NameValuePairsValueProvider base class, the one that allows a model binder to read values from a list of key/value pairs built from the QueryString parameters. The GetValue method of the IValueProvider is called later on from the model binder implementation’s BindModel method and returns a ValueProviderResult instance. By default, the QueryStringValueProviderFactory and the RouteDataValueProviderFactory are registered in the DefaultServices class. The model binders are also registered in the DefaultServices class for the ASP.NET Web API and assigned to the HttpParameterBinding, which then calls the model binder’s BindModel method inside the ExecuteBindingAsync method.

Customizing Model Binding

The process of model binding as described in the previous section might feel a bit complicated, so let’s customize the model binding to get a better understanding of it.

In order to read a value from a request header, you could query the request headers collection, much as was done with the QueryString parameters shown in Listing 12-54. This would come at the same cost as described there.

IValueProvider

A more generic approach using model binding involves implementing your own IValueProvider. The IValueProvider interface has the signature shown in Listing 12-62.

Listing 12-62.  IValueProvider Interface

public interface IValueProvider {
    bool ContainsPrefix(string prefix);
    ValueProviderResult GetValue(string key);
}

The ContainsPrefix method of the IValueProvider interface should return whether a prefix string is contained in the request (be it headers, QueryString, or anything else) and should be parsed. The GetValue method should return the value for a key being passed in to the method.

Our IValueProvider implementation should be able to read header information being passed in as nonstandard HTTP request headers, which start with the "X—" prefix. In our controller, we simply want to access a request header field name “X-Name” and return a string output, as shown in Listing 12-63.

Listing 12-63.  HeaderHelloController to Return a String Based on a Request Header Field

public class HelloController : ApiController {
    public string Get(string name) {
    return "Hello, " + name;
    }
}

In order to pass the X-Name header field into the Get method of the HelloController, we now need to implement our IValueProvider. Listing 12-64 shows the XHeaderValueProvider implementation.

Listing 12-64.  XHeaderValueProvider Implementation

public class XHeaderValueProvider : IValueProvider {
    private readonly HttpRequestHeaders _headers;
    private const string XHeaderPrefix = "X-";

    public XHeaderValueProvider(HttpActionContext actionContext) {
        _headers = actionContext.ControllerContext.Request.Headers;
    }

    public bool ContainsPrefix(string prefix) {
        return _headers.Any(header => header.Key.Contains(XHeaderPrefix + prefix));
    }

    public ValueProviderResult GetValue(string key) {
        IEnumerable<string> values;

        return _headers.TryGetValues(XHeaderPrefix + key, out values)
            ? new ValueProviderResult(
                values.First(),
                values.First(),
                CultureInfo.CurrentCulture)
            : null;
    }
}

The ContainsPrefix method implementation prefixes the incoming prefix (which is “Name” here because of the name controller method parameter) with an "X—" prefix, as we want to access only X-Header fields inside the request header collection. The header collection is created inside the constructor of our XHeaderValueProvider. Reading the content of the X-Name header field (if provided inside the request) is done using the GetValue method implementation. It queries the header field collection for the X-Name field and returns its value as a string.

In order to be able to make use of the XHeaderValueProvider, we also need—as described in the “Default Model Binder” section earlier—to implement a ValueProviderFactory. The implementation is pretty simple (see Listing 12-65), as it only returns a new instance of the XHeaderValueProvider when its GetValueProvider method is called.

Listing 12-65.  XHeaderValueProviderFactory Implementation

public class XHeaderValueProviderFactory : ValueProviderFactory {
    public override IValueProvider GetValueProvider(HttpActionContext actionContext) {
        return new XHeaderValueProvider(actionContext);
    }
}

There’s one small step left to get the X-Name field value passed as a string parameter to our controller’s Get method. The update is highlighted in Listing 12-66.

Listing 12-66.  Updated Version of HelloController

public class HelloController : ApiController {
    public string Get([ValueProvider(typeof(XHeaderValueProviderFactory))]string name) {
        return "Hello, " + name;
    }
}

The name parameter of the Get method is attributed now to the ValueProviderAttribute; it allows us to specify which ValueProviderFactory implementation (hence, which IValueProvider implementation) should try to provide that specific parameter.

When issuing a new request with Fiddler, providing an X-Name header field, as shown in Figure 12-7, we get the corresponding response.

9781430247258_Fig12-07.jpg

Figure 12-7 .  Debugging parameters from QueryString and request body content

As you now see how to implement a custom IValueProvider, let’s move to the next step: implementing a custom model binder that allows us to compose a model based on QueryString parameters.

IModelBinder

For the custom model binder implementation, we’ll reuse our scenario from Listings 12-57 to 12-59. Instead of decorating our Search parameter with the FromUri attribute inside our controller, we’ll decorate our Search entity with a ModelBinder attribute, as shown in Listing 12-67.

Listing 12-67.  Search Entity Decorated with the ModelBinder Attribute

[ModelBinder(typeof(SearchModelBinderProvider))]
public class Search {
    public string Text { get; set; }
    public int MaxResults { get; set; }
}

This tells the Web API model binding to look at the SearchModelBindingProvider implementation for which model binder to use to create an instance of the Search class from the requests parameters. Unfortunately, there is neither a SearchModelBinderProvider nor a corresponding model binder in ASP.NET Web API. But thanks to the IModelBinder interface, we can implement our custom model binder—it’s SearchModelBinderProvider.

The IModelBinder interface requires us to implement a single method, named BindModel, as you can see in Listing 12-68.

Listing 12-68.  IModelBinder Interface

public interface IModelBinder {    bool BindModel(HttpActionContext actionContext,
ModelBindingContext bindingContext);

}

The implementation of that interface is shown in Listing 12-68.

Listing 12-69.  SearchModelBinder Implementation

public class SearchModelBinder : IModelBinder {
    public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) {
        var model = (Search) bindingContext.Model ?? new Search();

        model.Text =
            bindingContext.ValueProvider.GetValue("Text").AttemptedValue;

        var maxResults = 0;
        if (int.TryParse(
            bindingContext
            .ValueProvider
            .GetValue("MaxResults").AttemptedValue, out maxResults)) {
                
            model.MaxResults = maxResults;
        }

        bindingContext.Model = model;

        return true;
    }
}

The SearchModelBinder implementation first checks whether the Model from the ModelBindingContext is null and creates a new Search instance if the model is null. Thereafter, the properties for the Search class instance are deserialized using the ValueProvider (it’s the build in QueryStringValueProvider here) from the QueryString of the incoming request. The Search instance, with its assigned properties Text and MaxResults, then will be passed back to the incoming BindingContext instance; true is returned from the BindModel method to indicate that the model has been bound correctly. Of course, in a real-world implementation you would make the return value depend on some validation and/or a try/catch block.

When running the modified API and issuing a new request to the URI from Listing 12-59 again, we get the same result as before, but it’s achieved this time using a different approach. The IModelBinder implementation shown has been quite simple; it’s up to you to build model binders for more complex scenarios. But keep in mind that deserializing the body content of the request is, by convention, the sole responsibility of MediaTypeFormatters in ASP.NET Web API.

As you can see, the model binding in ASP.NET Web API offers a well-thought-out set of implementations to provide a mechanism that not only gives us the possibility to bind body content using the media type formatters from the previous sections of the chapter but also allows us to bind parameters from other sections of the requests, like the QueryString or header fields. The icing on the cake is that you can even combine them to mix request body content and QueryString parameters within the same controller method.

Summary

Content negotiation is a huge topic in implementing HTTP or even RESTful applications. ASP.NET Web API supports all parts of the HTTP specification regarding content negotiation and abstracts that complexity away in an easy-to-use way by providing media type formatters for body content and model binding for QueryString parameters. If necessary, ASP.NET Web API also provides extensibility points where you can tweak content negotiation. It thus allows you to read and write every type of content you want to consume or provide in your Web API application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.217.187