Thursday, 16 April 2015

Use Media Type Formatters to make your Web API Controllers more testable

One of the benefits of moving to ASP.NET MVC from classic ASP.NET is the improved Separation of Concerns that can be achieved:
  • Controllers handle the retrieval and manipulation of the appropriate data models and the selection of the appropriate View to display the data.
  • Views handle the display of the data.
But why should you care about separation of concerns?
The most obvious benefit is that it makes your application more testable. You can easily create unit tests that create an instance of your controller class and call the action methods on it without needing to create or mock a HttpContext object.

The MVC framework includes many other interfaces and plug-points that can be used to ensure that Controllers are kept as free as possible from the "concern" of how incoming data is parsed from the HTTP request and the results of executing the controller method are written to the outgoing HTTP Response.  Some other time I'll write a blog about some of these....

...but today(!) I want to talk about a feature that the Web API has for ensuring better separation of concerns: Media Type Formatters. Web API is ASP.NET MVC's techie twin brother who has been specially adapted with bionic body parts that allow him to implement web services in really neat ways. In ASP.NET 6 Web API are MVC will be somehow merged into one, but for now they are two separate beasts.

What is a Media Type Formatter? I first encountered them when I created a web service method to add a document to a backing store:

1
2
3
4
5
6
        [HttpPut]
        public HttpResponseMessage AddDocument(string id,
            [FromBody] Stream content)
        {
            // Add document to backing store
        }

Looks sensible enough, but the first time I tried to call it I got a HTTP 415 Response: "Unsupported Media Type".

This is because Web API is designed to protect you from having to handle the incoming raw Stream.  The Web API has a collection of Media Type Formatters which is stored in
System.Web.Http.GlobalConfiguration.Configuration.Formatters

These are a bit like Model Binders in MVC - when a Web API method argument needs to be populated from an incoming request body Web API calls the MediaTypeFormatter.CanReadType(Type) method on each Media Type Formatter in turn, passing the type of the method argument, until it finds one that returns true. In my case none of the four built in Media Type Formatters were capable of deserialising a Stream to.... a Stream.

(NOTE: If you are reading this and are only interested in how you can make a Web API method that handles PUT requests accept content of any MIME Type then you may want to skip straight to my next post; if you're interested in how Media Type Formatters work for incoming content then keep reading this post before you go on to the next one.)

No problem, because it's easy to create a Media Type Formatter of your own!  Below is a basic Media Type Formatter that will allow you to have a method argument of type Stream:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web;

namespace WebApplication5
{
    public class StreamMediaTypeFormatter : MediaTypeFormatter
    {
        public StreamMediaTypeFormatter()
        {
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/octet-stream"));
        }

        public override bool CanReadType(Type type)
        {
            return typeof(Stream) == type;
        }

        public override bool CanWriteType(Type type)
        {
            return false;
        }

        public override Task<object> ReadFromStreamAsync(Type type, 
            Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
        {
            return Task.FromResult((object)readStream);
        }

        public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, 
            HttpContent content, IFormatterLogger formatterLogger, System.Threading.CancellationToken cancellationToken)
        {
            return ReadFromStreamAsync(type, readStream, content, formatterLogger);
        }
    }
}

(The CanWriteType method is there because Media Type Formatters are also used to serialise return types to the HTTP output stream.  In this post I'm only interested in deserialisation, but you may want to create a custom Media Type Formatter to serialise content too).

To tell the Web API to use our Media Type Formatter we need to add it to collection of formatters during application start up, like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    public class WebApiApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            GlobalConfiguration.Configure(WebApiConfig.Register);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);

            GlobalConfiguration.Configuration.Formatters.Insert(0, new StreamMediaTypeFormatter());
        }
    }

Note that we are inserting our new formatter at position 0 in the collection which means that it will take precedence over the existing formatters in the collection.

This is a good start but what if we would like our Web API to have access to some other information about the incoming Stream such as its length and the language of its contents?  These can be read from the Content-Length and Content-Language headers on the incoming request.  Now it would be possible to simply access the Request property from within our AddDocument method, like this:


1
2
3
4
5
6
7
        [HttpPut]
        public HttpResponseMessage AddDocument(string id, [FromBody]Stream value)
        {
            long contentLength = Request.Content.Headers.ContentLength.Value;
            string contentLanguage = Request.Content.Headers.ContentLanguage.FirstOrDefault();
            // Add document to backing store
        }

The problem with this is that it starts to break our principle of Separation of Concerns (or at least gnaw away at it): now the Controller needs to know something about the structure of the incoming HTTP Request and how to retrieve information from it. And this makes the Controller less testable; if we want to call this method from a unit test we would need to initialise the Request property of the Controller before calling it which is a little fiddly.

We could use IValueProviders to bind these header values to separate arguments on the Controller method (see http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api) but as these two values relate to the content of the Stream itself it seems neater to me to define a new type that encapsulates all the information about the content that we are interested in, StreamContentSource, and modify the Media Type Formatter to create an instance of that type. The type definition is:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Web;

namespace WebApplication5
{
    public class StreamContentSource : IContent
    {
        private Stream stream;

        public StreamContentSource(Stream stream, string contentLanguage, long contentLength)
        {
            this.stream = stream;
            this.ContentLength = contentLength;
            this.ContentLanguage = contentLanguage;
        }

        public Stream GetContentAsStream()
        {
            return stream;
        }

        public long ContentLength
        {
            get;
            private set;
        }

        public string ContentLanguage
        {
            get;
            private set;
        }
    }
}

The Media Type Formatter now looks like this:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web;

namespace WebApplication5
{
    public class StreamMediaTypeFormatter : MediaTypeFormatter
    {
        public StreamMediaTypeFormatter()
        {
            this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/octet-stream"));
        }

        public override bool CanReadType(Type type)
        {
            return typeof(StreamContentSource) == type;
        }

        public override bool CanWriteType(Type type)
        {
            return false;
        }

        public override Task<object> ReadFromStreamAsync(Type type, 
            Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
        {
            long contentLength = content.Headers.ContentLength.Value;
            string contentLanguage = content.Headers.ContentLanguage.FirstOrDefault();
            return Task.FromResult((object)new StreamContentSource(readStream, contentLanguage, contentLength));
        }

        public override Task<object> ReadFromStreamAsync(Type type, Stream readStream, 
            HttpContent content, IFormatterLogger formatterLogger, System.Threading.CancellationToken cancellationToken)
        {
            return ReadFromStreamAsync(type, readStream, content, formatterLogger);
        }
    }
}

And we modify our Controller method to accept an argument of type SteamContentSource.


1
2
3
4
5
6
        [HttpPut]
        public HttpResponseMessage AddDocument(string id, [FromBody]StreamContentSource value)
        {
            // Add document to backing store
            return new HttpResponseMessage(HttpStatusCode.Created);
        }

This gives us proper separation of concerns and makes our Controller method extremely testable.

UPDATE:
There are still some problems with this: our Media Type Formatter will only be invoked when the Content Type of the incoming request is application/octet-stream.  If this is not good for you then you need to read my next post.

photo credit: Whatever! via photopin (license)

No comments:

Post a Comment