Content Negotiation with ASP.NET Web API

Page content

This is a second post in the series all about ASP.NET Web API. The first post looked at getting started with the Web API. You can find the source code on Github.

An important part of Web API is resource content negotiation. The HTTP protocol RFC defines content negotiation as the process of selecting the best representation for a given response when there are multiple representations available. In practise the same resource can be represented in a variety of different ways – lets say a contact information resource can be shown in JSON representation, but also in XML or even as a PNG QR code containing the same content.

The content negotiation can be either server or client driven. In the first instance, the server decides what content to send down based on the various headers sent from the client. The latter requires an additional call to the server to get the list of available representations. Web API provides you with an easy way to send different representations of the same content.

Creating new MediaTypeFormatter to Deliver QR Code

The Web API comes with MediaTypeFormatter base class. If you want to create your own formatter, simply inherit from MediaTypeFormatter. If we take the existing scenario of contact management, lets say we want to create a QR code that contains the contact information of each contact stored in the database.

Our QrMediaFormatter can only be used for writing the type of Contact. Therefore we add logic to the CanWriteType and override the WriteToStreamAsync member.

 1public class QrMediaFormatter : MediaTypeFormatter
 3    private const string ApiEndpoint = "";
 5    public override bool CanReadType(Type type)
 6    {
 7        return false;
 8    }
10    public override bool CanWriteType(Type type)
11    {
12        if(type == null)
13        {
14            throw new ArgumentNullException("type");
15        }
17        return type == typeof(Contact);
18    }
20    public override Task WriteToStreamAsync(Type type, object value, Stream stream, System.Net.Http.HttpContent content, System.Net.TransportContext transportContext)
21    {
22        return Task.Factory.StartNew(() => WriteQrStream(type, value, stream, content.Headers));
23    }
25    private void WriteQrStream(Type type, object value, Stream stream, HttpContentHeaders contentHeaders)
26    {
27        var contact = value as Contact;
28        var values = new Dictionary<string, string>()
29                         {
30                             {"cht", "qr"},
31                             {"chs", "300x300"},
32                             {"chld", "H|0"},
33                         };
34        values.Add("chl", String.Format("{0}%0D%0A{1}", HttpUtility.UrlEncode(contact.Name), HttpUtility.UrlEncode(contact.Email)));
35        var endPointUrl = BuildUrl(values);
37        using(var client = new WebClient())
38        {
39            var data = client.DownloadData(endPointUrl);
40            stream.Write(data, 0, data.Length);
41        }
42    }
44    private string BuildUrl(IDictionary<string, string> values)
45    {
46        return string.Concat(ApiEndpoint, QueryString(values));
47    }
49    private string QueryString(IDictionary<string, string> queryStringItems)
50    {
51        var stringBuilder = new StringBuilder();
52        var joinCharacter = "?";
53        foreach (var key in queryStringItems.Keys)
54        {
55            stringBuilder.AppendFormat("{0}{1}={2}", joinCharacter, key, queryStringItems[key]);
56            joinCharacter = "&";
57        }
59        return stringBuilder.ToString();
60    }

The WriteQrStream method simply takes the Contact, builds up a simple string of the contact name and email and sends the string to the Google QR chart generation API. The stream coming from the API is then directly written to the response stream.

Wire Up Your Custom Media Type Formatter

The SupportedMediaTypes property contains a collection of MediaTypeHeaderValues. These define which media types the formatter can handle. In our case we are happy just to map the QR formatter to a single type –> image/png. However, it is possible to have the same media formatter handle a variety of MediaTypeHeaderValues. For example image/png and image/jpeg.

1public QrMediaFormatter()
3    SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/png"));   

Once we have the media type in the SupportedMediaTypes, the client can use the Content-Type request header to request the image representation of the resource. You can try it out in fiddler, accessing the contact resource with “Content-Type: image/png” header.

Map Extension to the Media Type Formatter

The idea is that the same resource should not change URI based on representation. However, it is difficult when some clients are unable to make requests for specific content type. For example, even if you use your contact resource as a path to an image /api/contact/a85ad33c-b61f-4503-8d75-861f3701efe3, the browser client does not send a content-type request for image and the service will the return a default representation of the resource, which in our case isn’t an image.

The way around it is to map a particular extension to the Media Type Formatter. It means adding an extension and therefore changing the URI, but it means it can be used by clients without specifying the requested content-type. First, you need to make sure your routes support extensions.

2    name: "IdWithExt",
3    routeTemplate: "api/{controller}/{id}.{ext}");

Then you need to add UriPathExtensionMapping to the MediaTypeMappings.

1public QrMediaFormatter()
3    MediaTypeMappings.Add(new UriPathExtensionMapping("png", "image/png"));
4    SupportedMediaTypes.Add(new MediaTypeHeaderValue("image/png"));   

Once you have both in place, you should be able to request the resource on /api/contact/a85ad33c-b61f-4503-8d75-861f3701efe3.png URI.

Change the Configuration to Add the QR Formatter

Finally, it is important to wire up all custom media formatters on app startup.

1GlobalConfiguration.Configuration.Formatters.Add(new QrMediaFormatter());

Code Sample

You can check out all the the above in the code sample on GitHub. If you have any questions give me a shout @mirajavora