Content Negotiation in ASP.NET MVC4 Web API Beta – Part 2

This is the second post in this series related to the Default Content Negotiation that ships as part of ASP.NET MVC4 Beta. I am continuing from where we left off in Part 1.

7. Up until now, we have seen how the Accept and Request Content-Type headers play a role in Conneg algorithm’s decision making process. There is one more way by which Conneg algorithm decides about the Response’s media type and formatter. It is called MediaTypeMapping. Every MediaTypeFormatter has a collection of these mappings in it.

MediaTypeMapping provides a way for the user to add some custom logic and decide whether they would want the formatter to take part in writing the Response. This is different from the default way of just matching media type values(like “application/xml”) present on the Accept and Request Content-Type headers and making decisions.

MediaTypeMapping provides a greater control.

Example:

Here I am creating a media type mapping which checks if the Request has a “Browser” header called “Internet Explorer”. If it finds the header, then it would inform the Conneg algorithm that it’s a full match and that the media type formatter having this media type mapping is ready to write the response.

 public class BrowserMediaTypeMapping : MediaTypeMapping
{
    public BrowserMediaTypeMapping() :
        base(new MediaTypeHeaderValue("application/xml"))
    {
    }

    /// <summary>
    /// NOTE: do NOT use this. It was too late for us to remove this method in Beta.
    /// It will be gone in RC though.
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    protected override double OnTryMatchMediaType(HttpRequestMessage request)
    {
        throw new NotImplementedException();
    }

    protected override double OnTryMatchMediaType(HttpResponseMessage response)
    {
        double match = 0.0; //no match

        //get the Request object associated with this Response
        HttpRequestMessage request = response.RequestMessage;

        //check the Request's headers for the Browser header
        IEnumerable<string> values = null;
        string browser = null;
        if (request.Headers.TryGetValues("Browser", out values))
        {
            browser = values.First();
        }

        if (browser != null && browser.ToLowerInvariant() == "internetexplorer")
        {
            match = 1.0; // full match
        }

        return match;
    }
}

As I mentioned earlier, each MediaTypeFormatter object has a collection of media type mappings available through the MediaTypeMappings property. So, we can add our BrowserMediaTypeMapping object in that collection like below:

 public SetupSelfHost()
{
    HttpSelfHostConfiguration config = new HttpSelfHostConfiguration(this.BaseAddress);
    config.Routes.MapHttpRoute("Default", "{action}", new { controller = "MappingTests" });

    config.Formatters.XmlFormatter.MediaTypeMappings.Add(new BrowserMediaTypeMapping());

    server = new HttpSelfHostServer(config);
    server.OpenAsync().Wait();
}

NOTE: In the above code snippet, I am just adding the mapping to the existing default formatter collection which has the formatters (in this order): JsonMediaTypeFormatter, XmlMediaTypeFormatter & FormUrlEncodedMediaTypeFormatter.

Now let’s see an example of how the Conneg algorithm behaves when a Request has the “Browser” header:

Request:

GET https://kirandev10:9090/MappingTests/GetString HTTP/1.1

Browser: InternetExplorer
Host: kirandev10:9090

Connection: Keep-Alive

Response:

HTTP/1.1 200 OK

Content-Length: 60

Content-Type: application/xml
Server: Microsoft-HTTPAPI/2.0

Date: Sun, 26 Feb 2012 23:58:43 GMT

<?xml version="1.0" encoding="utf-8"?><string>Hello</string>

In the above scenario, a GET request is sent without any Accept header. Usually if this kind of request is sent, Conneg algorithm would pick the 1st formatter in the list of formatters and that would be the JsonMediaTypeFormatter.

But, since the request has a “Browser” header with the value “Internet Explorer”, the XmlMediaTypeFormatter’s BrowserMediaTypeMapping checks this and responds to the Conneg algorithm by giving it a full match.

Hence we are seeing the Response in a “application/xml” format instead of the default “application/json” format.

As you can imagine, MediaTypeMapping provides full power to you to inspect incoming request and make a decision of whether to participate in a response or not.

By default, ASP.NET MVC4 Beta Web API ships with 4 built-in media type mappings. They are:

QueryStringMapping

RequestHeaderMapping

UriPathExtensionMapping

MediaRangeMapping

I will discuss more about these individually in my later posts.

What happens when multiple formatters match an incoming request’s criteria? Which one does the Conneg algorithm choose?

During the Conneg algorithm run, based on bunch of criteria like the Request Accept header, Content-Type header, MediaTypeMapping etc, there is always a possibility that more than one formatter could indicate its availability in writing the Response. As you can imagine, the Conneg algorithm has to choose only one formatter in the end.

The Default Conneg algorithm has the following precedence order to select the final formatter:

1. Formatter match based on Media Type Mapping.

2. Formatter match based on Request Accept header's media type.

3. Formatter match based on Request Content-Type header's media type.

4. Formatter match based on if it can serialize the response data’s Type.

In spite of all this, as mentioned in “Point 6. ”, if a user decides to always send a Response in a particular media type, Conneg algorithm will honor it and will not override it.