Kijana Woodard

Software Minimalism


Asp.net MVC Content Negotiation

Friday, November 8, 2013

It's long bothered me that I had a separate endpoint for Atom. After adding the archive endpoint, the absurdity really showed since is the same data, just in a different format.

Content negotiation to the rescue.

Web API has a decent conneg system built in. Fortunately, asp.net has enough extension points that we can craft a workable solution.

In order to get the 404 page working, I used a custom action invoker. I figured that could be used as the basis for content negotiation. I leaned on several sources to pull together the implementation.

The code needs a bit of work, but I'm happy with the effects. You can make a request to an endpoint with an appropriate accept header or by adding an extension to the url.

The result is that the atom feed uses the same endpoint as the archive you can see this post in json, xml, html, and partial html.

Hat tip to Joey Guerra for the extensions and phtml. He's pushed these ideas for years. I haven't always been receptive to them, but I felt there was enough value to implement them here.

One thing I learned doing this implementation is that the content negotiation should effect what action gets called rather, than being merely reactive to the action result.

For instance, I have code to do csv negotiation but it isn't being used because the current structure for posts isn't "flat" enough for csv. I considered some tricks using the mediator or cooking up some reflection magic to automatically flatten classes, but that seemed time consuming. Besides, what I really wanted was to get a clear opportunity in my controller to shape the output for a given mime type.

I am halfway there as I enable atom and xml to use a razor view to shape the output. For csv though I'd rather have something like:

public class MyController : Controller
{
    ...
    public object Csv(PostRequest request)
    {
        var model = _mediator.Send<PostRequest, PostGetViewModel>(request);
        return new SomeCsvShape
        {
           Title = model.Post.Title,
           ...
        };
    }
}   

Similarly rather than have a bunch of interfaces to support something like HAL, an action could decorate the regular model with links, etc.

In the end, I'd like content negotiation to have

  • some default behavior that may or may not work for your type
  • an outlet to shape the return result within a controller action
  • an outlet to shape the result with a custom view

Interestingly, I had nearly run out of reasons to have a controller class, other than because c# code has to be in a class. Content negotiation gives new perspective to controller cohesion.

A side note on scope creep. I spent a fair amount of time trying to work out pdf content negotiation. After hunting around, I found Rotativa, which looked promising, but I ran into a bug. It could be my issue, by while I was thinking about how to code my way out of this problem, it finally dawned on me: ctrl-p in chrome, plus a little css, pdf support done. :-]