Different MediaTypeFormatters for same MediaHeaderValue in ASP.NET Web API - StrathWeb

Strath

April 27th, 2012

Different MediaTypeFormatters for same MediaHeaderValue in ASP.NET Web API

How to use UriPathExtensionMapping and QueryStringMapping to vary your responses

Let’s say you have a model and want to serve it through a different MediaTypeFormatter from different controllers or routes or urls? You want same content type (MediaHeaderValue) request, to be formatted differently in different situations – how could you do that, if everything resides in GlobalConfiguration?

It would be perfect to be able to use per-controller configuration in ASP.NET Web API. Unfortunately, at this stage, this feature is not yet supported. Henrik mentioned that Mike Stall is currently working on this, and it will be supported in the full release on Web API (perhaps even earlier, on codeplex?).

Anyway, let’s take this idea for a spin and explore what we could do in beta version of ASP.NET Web API, because we could still vary our formatters and responses.

The idea

So last time we built a small Atom/RSS MediaTypeFormatter for the Web API. If you remember, we made it respond to requests with headers “application/atom+xml” and “application/rss+xml”, and set that in the GlobalConfiguration. That means that all the ApiControllers used that setup (so all requests with these content type headers would go through this formatter).

Now consider the following scenario (for brevity we are ditching Atom now and concentrate only on RSS). You have this nice RSS MediaTypeFormatter, yet in order for it to be used the client has to issue a request with “application/rss+xml”. You on the other, would like that MediaTypeFormatter to also be in use also when a user tries to get your RSS via the web browser (content type request “text/html”).

Let’s stop for a moment and add a new controller to the project (we are using project from last post as the start base), and call it RSSController. We will copy to it our repository property and two main Get() methods – to get all and get single model.

Your controller should look like this:

Again, in that case, the request will have a Content type “text/html”, and if you plain and simple add this content type handling to the MediaTypeFormatter it would handle all such requests with the RSS formatter, you just want the ones coming through RSSController. In other words, you’d like to handle the same content type with different MediaTypeFormatters, if they come through different controllers (routes).

Unfortunately, as mentioned, per-controller configurations are not supported yet, but even in beta, you still have some tools at your disposal that would help you to differentiate requests, and achieve what has been mentioned.

Option 1 – using UriPathExtensionMapping

To start off, we need to tell our MediaTypeFormatter that we will support “text/html” requests as well. To do that we overload the formatter’s constructor and add this new MediaTypeHeaderValue there.

Notice we used a generic argument, so that we can facilitate anything else as well, not just “text/html”, and we call AddUriPathExtensionMapping, telling the formatter to support the extension “rss”.

Right now what we need to do, is register our MediaTypeFormatter, but specifically for the “text/html” MediaHeaderValue (in Application_Start(), global.asax).

Since GlobalConfiguration by default gets 3 predefined Formatters (JsonMediaTypeFormatter , XmlMediaTypeFormatter, FormUrlEncodedMediaTypeFormatter), we want to keep them, and just insert one in front of them, hence we use the insert() method on the Formatters collection.

The final thing to do is to Register some routes that would allow us to make requests to our application with URIs that have an .rss extension.

We have two routes, cause we want to be able to request also individual items with rss extension.

Running this in the browser

So now let’s see what we have:
1. calling /api/values/ and /api/values/1 with content type “text/html” – our models in XML format as the default ASP.NET Web API formatter for text/xml (XmlMediaTypeFormatter ) would kick in

This is no news since this is how our controller behaved last time. Notice that in this case “text/html” request is handled by XmlMediaTypeFormatter.

2. calling /api/rss.rss and /api/rss/1.rss with content type “text/html” – our models in RSS format since only *.rss requests will spit out the models through our custom MediaTypeFormatter for this content type. All other requests with “text/html” would use GlobalConfiguration defaults (as in point 2).

Now this is something! We can easily see that our extension specific MediaTypeFormatter works like a charm. It is not only extension specific (.rss) but also content type specific at the same time (“text/html” only). So – same MediaHeaderValue, but different formatter in play!

This way we showed, it is easily possible to use different MediaTypeFormatters for same content type, even without support for per-controller configuration.

Option 2 – using QueryStringMapping

This is a bit less elegant option, since it would require your clients’ requests to contain QueryStrings, but it works very well.

In principle, responses from our ApiController will be formatted by Default formatter (whatever it is in the GlobalConfiguration Formatters collection at the moment), until you pass an API request with a QueryString parameter. If it matches the settings predefined in the QueryStringMapping of your MediaTypeFormatter, that formatting will kick in.

All that’s needed to enable it is to add a call to AddQueryStringMapping() in the constructor of the MediaTypeFormatter, and specify the QueryString key and value to be expected. Notice that AddUriPathExtensionMapping and AddQueryStringMapping can easily run side-by-side, since one deals with extension in the URI and the other with QueryString.

Running this in the browser

Again let’s run all some examples.
1. calling /api/values/ and /api/values/1 with content type “text/html” – our models in XML format as the default ASP.NET Web API formatter would kick in

Again, the default behavior remains unaffected.

2. calling /api/values/?formatter=rss and /api/values/1?formatter=rss with content type “text/html” – our models in RSS format since the custom MediaTypeFormatter kicks in

Summary & source code

As you see, there is quite some flexibility in formatting the responses from ASP.NET Web API, and even though the per-controller Configuration is not currently supported, you can still do quite a lot very easily (out of the box) with things like PathExtensionMapping and QueryStringMapping. Have fun.

- Source code (ZIP, 6.5MB) – VS11 project

Be Sociable, Share!

  • Tim

    Very good post, thanks Filip.

  • Pingback: Dew Drop – April 30, 2012 (#1,316) | Alvin Ashcraft's Morning Dew

  • Jeremy S

    I pretty much used your code verbatim (abstracting it to a more generalized class). Works fine, except I have a strange issue I hope you may be able to shed some light upon.

    For testing, I had combined it with a console wrapper I found (http://www.johnnycode.com/blog/2012/02/23/consuming-your-own-asp-net-web-api-rest-service/).

    I set it up to only apply the custom rss formatting on a Products controller, leaving Values and MyData controllers alone. It seems to work fine when requesting directly through the browser, but the Console app’s web requests for the Product controller are *always* running the RSS formatter, even when I’m not requesting it (as far as I can tell).

    Would you know how I can figure out where the format request could be coming from? If you’re interested I can send you the project example as well.

    • Filip W

      Hi, as usually, it’s hard to say anything without looking at any of the aforementioned console code.

      If you can post some bits and pieces of email me (my email is in the right hand column) I’d be happy to have a look.

      cheers
      /f

  • Dan Stevens

    Great post Filip, it helped me a lot. One part gave me problems though, using {extension} in the routeTemplate did not work for me, but once I changed it to {ext} it worked like a charm.

  • Patrick

    in order to use outlook for reading feeds, you have to provide this .rss extension