Saturday, 28 April 2012

ASP.NET Web API Series - Part 5: MediaTypeFormatter explained


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.

MediaTypeFormatter as we will see will bridge the gap between these two abstractions.

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 Web API
Tower Bridge of ASP.NET Web API

A media type formatter in brief:
  1. 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)
  2. Tells Web API whether it can read or write a type (for Controller world)
  3. Has an understanding of encoding/charset which is passed in the HTTP header and can read accordingly
  4. It will be given a stream to read (from request) or write (to response)
  5. 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)
  6. 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):

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.

10 comments:

  1. Awesome post.
    Have question on my project.

    I've CoursesController, in that i've post method:
    public Course Post(Course course)
    {
    course.id = _courses.Count;
    _courses.Add(course);

    return course;
    }


    When i call post method from fiddler, i get 'No MediaTypeformatter' error.
    I'm not able to understand why as i'm passing as json object why my api can't find formatter?

    Please help!

    Request:
    POST http://localhost:64009/api/courses HTTP/1.1
    User-Agent: Fiddler
    Host: localhost:64009
    Content-Length: 25

    {"id":1,"name":"course1"}

    Response:
    {"ExceptionType":"System.InvalidOperationException","Message":"No 'MediaTypeFormatter' is available to read an object of type 'Course' with the media type ''undefined''.","StackTrace":" at System.Net.Http.ObjectContent.SelectAndValidateReadFormatter(Boolean acceptNullFormatter)\u000d\u000a at System.Net.Http.ObjectContent.ReadAsyncInternal[T](Boolean allowDefaultIfNoFormatter)\u000d\u000a at System.Web.Http.ModelBinding.RequestContentReader.ReadWholeBodyAsync(HttpActionContext actionContext, Type modelType)\u000d\u000a at System.Web.Http.ModelBinding.RequestContentReader.ReadContentAsync(HttpActionContext actionContext)\u000d\u000a at System.Web.Http.ModelBinding.DefaultActionValueBinder.BindValuesAsync(HttpActionContext actionContext, CancellationToken cancellationToken)\u000d\u000a at System.Web.Http.ApiController.<>c__DisplayClass3.b__0()\u000d\u000a at System.Web.Http.ApiController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)\u000d\u000a at System.Web.Http.Dispatcher.HttpControllerDispatcher.SendAsyncInternal(HttpRequestMessage request, CancellationToken cancellationToken)\u000d\u000a at System.Web.Http.Dispatcher.HttpControllerDispatcher.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)"}

    ReplyDelete
    Replies
    1. If you are using beta, Microsoft implementation on JSON is very strict and you need every key or value in quotes so instead of

      {"id":1,"name":"course1"}

      send

      {"id":"1","name":"course1"}

      Delete
  2. Your post is Really worth to read it and I really Enjoyt it, Please keep it up.


    SEO Sydney | SEO Melbourne | SEO

    ReplyDelete
  3. This comment has been removed by a blog administrator.

    ReplyDelete
  4. Hi, we're using an implementation of BinaryMediaTypeFormatter to post data (IEnumerable) serialized and deserialized in a particular way. Unfortunately, we're very slow: 300 ms is our process time in the controller, but we can see in the IIS log a total of 10000 ms for the same request!
    The async version could help us?
    Thanks!

    ReplyDelete
    Replies
    1. Sure. Show me the code and I should be able to help. my email is aliostad [[AT]] gmail dot com

      Delete
  5. do you have any examples of a GET call that returns form data which includes a byte binary image? can you take a look at my stackoverflow post? it has been hard finding someone that can help me.

    http://stackoverflow.com/questions/23001290/retrieve-form-data-that-includes-a-binary-image-and-display-in-view-using-webapi

    any help will be appreciated. thank you

    ReplyDelete