Handling Content Type as we should, through HTTP Headers

In the previous post I spiked a solution to return different result types depending on a requested format. While the solution I came up with was sufficient to address the points issued on a thread at .NetArchitects, Eric Hexter hit the nail on the head with the following tweet:

@pedroreys nice post on the formatter. I would consider http headers instead of routing, although that is hard to manually debug

The right way to specify the types which are accepted for the response is by using the Accept request-header. It’s been a while since Eric’s tweet, but here is the solution I came up with.

Lemme first define the expectations I defined:

If the Accept header of the request

If the requested content type can not be handled by none of the previous statements, a response with a HTTP Status Code 406 – Not Acceptable should be returned.

For this post. I will start with the same controller class that I used in the previous one:


public class ClientController : Controller
{
    public ActionResult Index()
    {
        var clients = new[] {
        new Client {FirstName = "John", LastName = "Smith"},
        new Client {FirstName = "Dave", LastName = "Boo"},
        new Client {FirstName = "Garry", LastName = "Foo"}
                           };

        return View(clients);
    }

}

public class Client
{
    public string FirstName{get;set;}

    public string LastName{get; set; }
    
}

In order to be able to format the result based on the requested content-type on the HTTP header, I will create a base controller class that will provide this functionality to the controller. I will call it ContentTypeAwareController.


public class ContentTypeAwareController : Controller
{
   public virtual IContentTypeHandlerRepository HandlerRepository { get; set; } 

    protected ActionResult ResultWith(object model)
        {
            var handler = HandlerRepository.GetHandlerFor(HttpContext);
            return handler.ResultWith(model);
        }
}

That’s it. This base class is responsible just to ask the Handler Repository for the right handler to the given HttpContext and call the ResultWith() method on the returned handler.

The IContentTypeHandler interface is pretty simple as well:


public interface IContentTypeHandler
{
    bool CanHandle(HttpContextBase context);
    ActionResult ResultWith(object model);
}

Simple and intuitive, right?

Let’s implement this interface then, but first let’s set the expectations as unit tests:


[TestFixture] 
public class JsonHandlerTest : HandlerTestBase 
{ 
    [Test] 
    public void should_return_a_JsonResult() 
    { 
        var handler = new JsonHandler(); 
        var result = handler.ResultWith("whatever"); 
        result.ShouldBeType<JsonResult>(); 
    } 

    [Test] 
    public void should_Handle_request_when_content_type_requested_contains_Json() 
    { 
        Headers.Add("Accept", "application/json"); 

        var handler = new JsonHandler(); 
        handler.CanHandle(HttpContext).ShouldBeTrue(); 
    } 

    [Test] 
    public void should_not_handle_request_when_content_type_requested_does_not_contain_json() 
    { 
        Headers.Add("Accept", "text/xml"); 
        var handler = new JsonHandler(); 
        handler.CanHandle(HttpContext).ShouldBeFalse(); 
    } 
}


To keep the post short, I will put here just the test above, all the other tests are in the solution on github.

The implementation of the JsonHandler is below:

public class JsonHandler : IContentTypeHandler
{
    public const string _acceptedType = "application/json";
    public bool CanHandle(HttpContextBase context)
    {
        var acceptHeader = context.Request.Headers.Get("Accept"); 

        return acceptHeader.Contains(_acceptedType);
    } 

    public ActionResult ResultWith(object model)
    {
       return new JsonResult
       {
           Data = model,
           ContentEncoding = null,
           JsonRequestBehavior = JsonRequestBehavior.AllowGet
       };
    }
}

It’s important to set the value for the JsonRequestBehavior property on the JsonResult object. Otherwise you will end-up with a InvalidOperationException.

The other handlers are pretty straight-forward as well.

The one to handle requests for Xml:

public class XmlHandler : IContentTypeHandler
{
    public const string _acceptedType = "text/xml";

    public bool CanHandle(HttpContextBase context)
    {
        var acceptHeader = context.Request.Headers.Get("Accept");

        return acceptHeader.Contains(_acceptedType);
    }

    public ActionResult ResultWith(object model)
    {
        return new XmlResult(model);
    }
}

And the other to be the default, Html handler:

public class HtmlHandler : IContentTypeHandler
{
    public const string _acceptedType = "text/html";
    
    public bool CanHandle(HttpContextBase context)
    {
        var acceptHeader = context.Request.Headers.Get("Accept");

        return acceptHeader.Contains(_acceptedType);
        
    }

    public ActionResult ResultWith(object model)
    {
        return new ViewResult
       {
           ViewData = new ViewDataDictionary(model),
       };
    }
}

And finally, we will have a handler to return the 406 code in case of the others handlers not being able to handle the requested content type.

public class NotAcceptedContentTypeHandler : IContentTypeHandler
{
    public bool CanHandle(HttpContextBase context)
    {
        return true;
    }

    public ActionResult ResultWith(object model)
    {
        return new NotAcceptedContentTypeResult();
    }
}

With all that code in place, all we have left to do is to make the ClientController to inherit from the ContentTypeAwareController and call the ResultWith() method:

public class ClientController : ContentTypeAwareController 
{
    public ActionResult Index()
    {
        var clients = new[]{
            new Client {FirstName = "John", LastName = "Smith"},
            new Client {FirstName = "Dave", LastName = "Boo"},
            new Client {FirstName = "Garry", LastName = "Foo"}
                           };

            return ResultWith(clients);
        }

    }

That’s it. The Client Controller now is able to respond to Json, Xml or Html requests. Let’s check it.

The web page is still being rendered as expected. Fine.

But now, let’s see what responses do I get when I change the Http Header to “application/json”:

Yeah, a Json result. Cool. What about “text/xml”:

Yeap, that works too. Finally, let’s see if the 406 status code is returned when an invalid content type is requested:

So, with not so much code the Client Controller now is able to format its result based on the Content Type requested on the HTTP Header, as it should have been before. Off course this is a spike and the code can be improved. Feel free to grab it from github and improve it.

comments powered by Disqus