Vendor Media Types With the ASP.NET Web API by Mark Seemann
In RESTful services, media types (e.g. application/xml, application/json) are an important part of Content Negotiation (conneg in the jargon). This enables an API to provide multiple representations of the same resource.
Apart from the standard media types such as application/xml, application/json, etc. an API can (and often should, IMO) expose its resources using specialized media types. These often take the form of vendor-specific media types, such as application/vnd.247e.catalog+xml or application/vnd.247e.album+json.
In this article I'll present some initial findings I've made while investigating this in the ASP.NET Web API (beta).
For an introduction to conneg with the Web API, see Gunnar Peipman's ASP.NET blog, particularly these two posts:
- ASP.NET Web API: How content negotiation works?
- ASP.NET Web API: Extending content negotiation with new formats
The Problem #
In a particular RESTful API, I'd like to enable vendor-specific media types as well as the standard application/xml and application/json media types.
More specifically, I'd like to add these media types to the API:
- application/vnd.247e.album+xml
- application/vnd.247e.artist+xml
- application/vnd.247e.catalog+xml
- application/vnd.247e.search-result+xml
- application/vnd.247e.track+xml
- application/vnd.247e.album+json
- application/vnd.247e.artist+json
- application/vnd.247e.catalog+json
- application/vnd.247e.search-result+json
- application/vnd.247e.track+json
However, I can't just add all these media types to GlobalConfiguration.Configuration.Formatters.XmlFormatter.SupportedMediaTypes or GlobalConfiguration.Configuration.Formatters.JsonFormatter.SupportedMediaTypes. If I do that, each and every resource in the API would accept and (claim to) return all of those media types. That's not what I want. Rather, I want specific resources to accept and return specific media types.
For example, if a resource (Controller) returns an instance of the SearchResult (Model) class, it should only accept the media types application/vnd.247e.search-result+xml, application/vnd.247e.search-result+json (as well as the standard application/xml and application/json media types).
Likewise, a resource handling the Album (Model) class should accept and return application/vnd.247e.album+xml and application/vnd.247e.album+json, and so on.
Figuring out how to enable such behavior took me a bit of fiddling (yes, Fiddler was involved).
The Solution #
The Web API uses a polymorphic collection of MediaTypeFormatter classes. These classes can be extended to be more specifically targeted at a specific Model class.
For XML formatting, this can be done by deriving from the built-in XmlMediaTypeFormatter class:
public class TypedXmlMediaTypeFormatter : XmlMediaTypeFormatter { private readonly Type resourceType; public TypedXmlMediaTypeFormatter(Type resourceType, MediaTypeHeaderValue mediaType) { this.resourceType = resourceType; this.SupportedMediaTypes.Clear(); this.SupportedMediaTypes.Add(mediaType); } protected override bool CanReadType(Type type) { return this.resourceType == type; } protected override bool CanWriteType(Type type) { return this.resourceType == type; } }
The implementation is quite simple. In the constructor, it makes sure to clear out any existing supported media types and to add only the media type passed in via the constructor.
The CanReadType and CanWriteType overrides only return true of the type parameter matches the type targeted by the particular TypedXmlMediaTypeFormatter instance. You could say that the TypedXmlMediaTypeFormatter provides a specific match between a media type and a resource Model class.
The JSON formatter is similar:
public class TypedJsonMediaTypeFormatter : JsonMediaTypeFormatter { private readonly Type resourceType; public TypedJsonMediaTypeFormatter(Type resourceType, MediaTypeHeaderValue mediaType) { this.resourceType = resourceType; this.SupportedMediaTypes.Clear(); this.SupportedMediaTypes.Add(mediaType); } protected override bool CanReadType(Type type) { return this.resourceType == type; } protected override bool CanWriteType(Type type) { return this.resourceType == type; } }
The only difference from the TypedXmlMediaTypeFormatter class is that this one derives from JsonMediaTypeFormatter instead of XmlMediaTypeFormatter.
With these two classes available, I can now register all the custom media types in Global.asax like this:
GlobalConfiguration.Configuration.Formatters.Add( new TypedXmlMediaTypeFormatter( typeof(Album), new MediaTypeHeaderValue( "application/vnd.247e.album+xml"))); GlobalConfiguration.Configuration.Formatters.Add( new TypedXmlMediaTypeFormatter( typeof(Artist), new MediaTypeHeaderValue( "application/vnd.247e.artist+xml"))); GlobalConfiguration.Configuration.Formatters.Add( new TypedXmlMediaTypeFormatter( typeof(Catalog), new MediaTypeHeaderValue( "application/vnd.247e.catalog+xml"))); GlobalConfiguration.Configuration.Formatters.Add( new TypedXmlMediaTypeFormatter( typeof(SearchResult), new MediaTypeHeaderValue( "application/vnd.247e.search-result+xml"))); GlobalConfiguration.Configuration.Formatters.Add( new TypedXmlMediaTypeFormatter( typeof(Track), new MediaTypeHeaderValue( "application/vnd.247e.track+xml"))); GlobalConfiguration.Configuration.Formatters.Add( new TypedJsonMediaTypeFormatter( typeof(Album), new MediaTypeHeaderValue( "application/vnd.247e.album+json"))); GlobalConfiguration.Configuration.Formatters.Add( new TypedJsonMediaTypeFormatter( typeof(Artist), new MediaTypeHeaderValue( "application/vnd.247e.artist+json"))); GlobalConfiguration.Configuration.Formatters.Add( new TypedJsonMediaTypeFormatter( typeof(Catalog), new MediaTypeHeaderValue( "application/vnd.247e.catalog+json"))); GlobalConfiguration.Configuration.Formatters.Add( new TypedJsonMediaTypeFormatter( typeof(SearchResult), new MediaTypeHeaderValue( "application/vnd.247e.search-result+json"))); GlobalConfiguration.Configuration.Formatters.Add( new TypedJsonMediaTypeFormatter( typeof(Track), new MediaTypeHeaderValue( "application/vnd.247e.track+json")));
This is rather repetitive code, but I'll leave it as an exercise to the reader to write a set of conventions that appropriately register the correct media type for a Model class.
Caveats #
Please be aware that I've only tested this with a read-only API. You may need to tweak this solution in order to also handle incoming data.
As far as I can tell from the Web API source repository, it seems as though there are some breaking changes in the pipeline in this area, so don't bet the farm on this particular solution.
Lastly, it seems as though this solution doesn't correctly respect opt-out quality parameters in incoming Accept headers. As an example, if I request a 'Catalog' resource, but supply the following Accept header, I'd expect the response to be 406 (Not Acceptable)
.
Accept: application/vnd.247e.search-result+xml; q=1, */*; q=0.0
However, the result is that the service falls back to its default representation, which is application/json
. Whether this is a problem with my approach or a bug in the Web API, I haven't investigated.
Comments
http://pedroreys.com/2012/02/17/extending-asp-net-web-api-content-negotiation/
https://gist.github.com/2499672
You might find this useful.