Different MediaTypeFormatters for same MediaHeaderValue in ASP.NET Web API

· 1104 words · 6 minutes to read

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:

public class RSSController : ApiController  
{  
static readonly IUrlRepository _repo = new UrlRepository();  
public IEnumerable<Url> Get()  
{  
return _repo.GetAll();  
}  
public Url Get(int id)  
{  
return _repo.Get(id);  
}  
}  

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.

private readonly string atom = "application/atom+xml";  
private readonly string rss = "application/rss+xml";

public SyndicationFeedFormatter()  
{  
SupportedMediaTypes.Add(new MediaTypeHeaderValue(atom));  
SupportedMediaTypes.Add(new MediaTypeHeaderValue(rss));  
}

public SyndicationFeedFormatter(string format)  
{  
this.AddUriPathExtensionMapping("rss", new MediaTypeHeaderValue(format));  
}  

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).

GlobalConfiguration.Configuration.Formatters.Insert(0, new SyndicationFeedFormatter("text/html"));  

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.

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

routes.MapHttpRoute(  
name: "Api UriPathExtension ID",  
routeTemplate: "api/{controller}/{id}.{extension}",  
defaults: new { id = RouteParameter.Optional, extension = RouteParameter.Optional }  
);  

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.

  1. 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.

public SyndicationFeedFormatter(string format)  
{  
this.AddUriPathExtensionMapping("rss", new MediaTypeHeaderValue(format));  
this.AddQueryStringMapping("formatter", "rss", new MediaTypeHeaderValue(format));  
}  

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.

  1. 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.

About


Hi! I'm Filip W., a software architect from Zürich 🇨🇭. I like Toronto Maple Leafs 🇨🇦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁢󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP