Introduction
[Level T2] MediaTypeFormatter is an exciting concept introduced in the ASP.NET Web API which will enable seamless conversion of HTTP data to/from.NET types. This post reviews the concepts and basic usage of MediaTypeFormatter in the ASP.NET Web API pipeline. This is an area of Web API which is being actively developed so the content of this post might be updated to reflect the changes - but this post at the time of publishing is based on the latest source code available.
Background
HTTP abstracts a resource (identified by a URI which is commonly a URL) from its representation. A resource e.g. an employee detail can be identified by /employees/123. An HTTP agent (client) and a server engage in content negotiation to decide on the best format it can be represented. For example, a client can express its wishes to receive employee detail in plain text (by specifying content-type header of text/plain), RTF, XML, JSON or even image.
On the other hand, ASP.NET Web API has also abstracted away parameters or result of an action form its representation. While ASP.NET MVC had this feature for input parameters, return type should have been an instance of ActionResult hence controller had to make a decision on the format of resource by returning ContentResult, JsonResult, etc.
What is Media Type?
As you all probably know, media type refers to the value of the content-type header within an HTTP request and response. Media types allow agent (client) and server to define the type of the data sent in the HTTP body (payload). It is also used within the accept header in the request to allow content negotiation, i.e. client notifies the server of the media types it accepts/prefers. I will need to have a separate post on content negotiation but as for now, this little introduction suffices.
There are standard media types as listed in the Wiki link. There is no limitation on the media types and you can come up with your own media types but these media types usually start with X-.
A request or response does not have to have a single media type. It can mix the media types but in this case it has to use multipart content-type (value of the content type will be multipart/mixed) so that each part defines its content type.
What is MediaTypeFormatter?
Media type formatter is the bridge between the HTTP world of URI fragments, headers and body on one side, and the controller world of actions, parameters and return types.
Tower Bridge of ASP.NET Web API |
A media type formatter in brief:
- Has a knowledge of one or more media type (for example text/xml and application/xml both refer to the same structure which is XML) and tells Web API which content types it supports (for the HTTP world)
- Tells Web API whether it can read or write a type (for Controller world)
- Has an understanding of encoding/charset which is passed in the HTTP header and can read accordingly
- It will be given a stream to read (from request) or write (to response)
- Its work usually (but not always) involves serialisation (at the time writing to the response) or deserialisation (at the time of reading from the request)
- Inherits abstract class MediaTypeFormatter
MediaTypeFormatter class
MediaTypeFormatter class in the ASP.NET Web API is an abstract class providing base services for various media type formatters.
Important properties and methods include (more informative as code):
public abstract class MediaTypeFormatter { // properties public Collection<MediaTypeHeaderValue> SupportedMediaTypes { get; private set; } public Collection<Encoding> SupportedEncodings { get; private set; } public Collection<MediaTypeMapping> MediaTypeMappings { get; private set; } // methods public virtual Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger) { // to be overriden by base class } public virtual Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext) { // to be overriden by base class } public abstract bool CanReadType(Type type); public abstract bool CanWriteType(Type type); }
Things to note above are:
- As with the rest of the Web API, MediaTypeFormatter fully supports Async using TPL. Having said that, most implementations of MediaTypeFormatter run synchronously as they involve serialisation which is safe as a synchronous operation.
- SupportedMediaTypes defines a list of media type headers that an implementation supports. For example application/xml and text/xml
- MediaTypeMappings is an interesting concept where a media type formatter can define its preference for a particular media type based on a value in the request (query string, URI fragment, HTTP header). A typical example is existence of x-requested-with header which signals the AJAX based request hence JSON is the preferred content-type.
How ASP.NET Web API uses formatters?
Media type formatters are global formatters sitting in the Formatters property of HttpConfiguration. If you are using ASP.NET hosting (IIS, Cassini, etc) then you may use GlobalConfiguration.Configuration to access the instance of HttpConfiguration containing Formatters. If you are using Self-Hosting, then you would be creating a HttpSelfHostConfiguration object which you will use its Formatters property.
This snippet will output all formatters that are setup by default in the ASP.NET Web API:
foreach (var formatter in config.Formatters) { Trace.WriteLine(string.Format("{0}: {1}", formatter.GetType().Name, string.Join(", ", formatter.SupportedMediaTypes.Select(x=>x.MediaType)) )); }
And here is the output (at the time of writing this blog):
Since we have not yet added our formatter, we get back this error:
JsonMediaTypeFormatter: application/json, text/json
XmlMediaTypeFormatter: application/xml, text/xml
FormUrlEncodedMediaTypeFormatter: application/x-www-form-urlencoded
JQueryMvcFormUrlEncodedFormatter: application/x-www-form-urlencoded
This list is very much likely to be extended by the time ASP.NET Web API is shipped.
You might be surprised to see two different media type formatters targeting the same media type. But that is very normal: media type formatters compete for becoming the formatter of choice! If ASP.NET Web API find two formatters for the same content type, it will pick the first one so it is very important to add formatters in the right order.
Writing a simple BinaryMediaTypeFormatter
Now, we want to get our hands dirty and implement a useful formatter that is not currently provided by the ASP.NET Web API. This formatter will be able to formatter application/octet-stream media type in the HTTP world to the byte[] type in the controller world.
Let's imagine we have a controller that calculates SHA1 hash of the small binary data posted to it (this is a good practice for large streams):
public class BinaryController : ApiController { [HttpPost] public string CalculateHash(byte[] data) { using(var sha1 = new SHA1CryptoServiceProvider()) { return Convert.ToBase64String(sha1.ComputeHash(data)); } } }
In our implementation, we use synchronous approach, although in this case it is safe to use asynchronous as there is no serialisation taking place. However, since this is intended only for small payloads, context switching of the asynchronous TPL has more overhead - in any case turning this code into asynchronous is very easy: an alternate implementation supporting async can be found here.
public class BinaryMediaTypeFormatter : MediaTypeFormatter { private static Type _supportedType = typeof (byte[]); private const int BufferSize = 8192; // 8K public BinaryMediaTypeFormatter() { SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/octet-stream")); } public override bool CanReadType(Type type) { return type == _supportedType; } public override bool CanWriteType(Type type) { return type == _supportedType; } public override Task<object> ReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders, IFormatterLogger formatterLogger) { var taskSource = new TaskCompletionSource<object>(); try { var ms = new MemoryStream(); stream.CopyTo(ms, BufferSize); taskSource.SetResult(ms.ToArray()); } catch (Exception e) { taskSource.SetException(e); } return taskSource.Task; } public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext) { var taskSource = new TaskCompletionSource<object>(); try { if (value == null) value = new byte[0]; var ms = new MemoryStream((byte[]) value); ms.CopyTo(stream); taskSource.SetResult(null); } catch (Exception e) { taskSource.SetException(e); } return taskSource.Task; } }
Using BinaryMediaTypeFormatter
Now let's use our formatter. You need a REST console of your choice (Chrome REST console, REST Sharp library, Fiddler) to send this request:
POST http://localhost:7777/api/Binary HTTP/1.1 User-Agent: Fiddler Host: localhost:7777 content-type: application/octet-stream Content-Length: 14 This is a test
Since we have not yet added our formatter, we get back this error:
No MediaTypeFormatter is available to read an object of type 'Byte[]' from content with media type 'application/octet-stream'.
So we just need to add our formatter:
config.Formatters.Add(new BinaryMediaTypeFormatter());
And we will get back this response:
HTTP/1.1 200 OK Content-Length: 30 Content-Type: application/json; charset=utf-8 Server: Microsoft-HTTPAPI/2.0 Date: Sat, 28 Apr 2012 12:09:44 GMT "pU2I4GYS2CC8O+cod8dPJXtWGxk="
As you can see, the response content type is application/json. I have explained in my post Part 1 why it is the case: JsonMediaTypeFormatter is the default media type formatter. Now we know why it is the case: it is the first item in the collection (see above).
Conclusion
Media type formatter is the bridge between the HTTP world of URI, headers and body on one side, and the controller world of actions, parameters and return types. In ASP.NET Web API, it is represented by abstract class MediaTypeFormatter. Order of formatters in the Formatters property of HttpConfiguration is important when ASP.NET Web API has to choose between two formatters supporting the same media type.