July 6, 2012

Content negotiation in ASP.NET MVC

A few weeks ago I posted about how we encapsulate commmon behaviour in ASP.NET MVC using Action Filters.

The example code demonstrated how to return JSON if requested by the client. Of course checking the accept headers within the action is bad (that was the point of my post) as it means to support new formats we would have to modify the action.

As a response to this code Fredrik Normén demonstrated a nice way of using Razor with ASP.NET Web API. Out of the box Web API is much better suited for content negotiation. By adding a custom MediaTypeFormatter Fredrik was able to return HTML generated by Razor by navigating to the API endpoint in his browser (which I actually think is a great way of providing API documentation).

That said, content negotiation can be achieved in ASP.NET MVC very easily. Since it’s better suited for rendering HTML than Web API I would say that ASP.NET MVC is a better solution if you have an API that does need to return HTML along with other common formats (JSON etc.).

I’ve just pushed a draft of my implementation to Fabrik.Common. Adding content negotiation to a Controller Action is as simple as decorating it with an attribute:

[AutoFormatResult]
public ActionResult About()
{
    var locations = new[] { "United Kingdom", "Belgium", "United States" };
    return View(locations);
}

If we request this page using our web browser we’ll get the normal HTML result we expect.

To return other formats we just need to set our accept headers to the appropriate media type. So setting to application/json will return JSON and text/xml will return XML. I’m using RESTClient to test:

application/json

[
   "United Kingdom",
   "Belgium",
   "United States"
]

text/xml

<?xml version="1.0" encoding="utf-8"?>
<ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
			   xmlns:xsd="http://www.w3.org/2001/XMLSchema">
	<string>United Kingdom</string>
	<string>Belgium</string>
	<string>United States</string>
</ArrayOfString>

Adding your own formatter is simply a case of implementing MediaTypeViewResultFormatter, or IViewResultFormatter if you want more control. Below is the PartialViewResultFormatter (not added by default) that automatically returns a Partial View if the request is made via AJAX or is a Child Action:

public class PartialViewResultFormatter : IViewResultFormatter
{
    private readonly string partialViewPrefix;

    public PartialViewResultFormatter(string partialViewPrefix = "_")
    {
        Ensure.Argument.NotNull(partialViewPrefix, "partialViewPrefix"); // can be empty
        this.partialViewPrefix = partialViewPrefix;
    }
    
    public bool IsSatisfiedBy(ControllerContext controllerContext)
    {
        return controllerContext.HttpContext.Request.AcceptTypes.Contains("text/html")
            && (controllerContext.HttpContext.Request.IsAjaxRequest() || controllerContext.IsChildAction);
    }

    public ActionResult CreateResult(ControllerContext controllerContext, ActionResult currentResult)
    {
        var viewResult = currentResult as ViewResult;

        if (viewResult == null)
            return null;

        var viewName = viewResult.ViewName.NullIfEmpty() 
            ?? controllerContext.RequestContext.RouteData.GetRequiredString("action");

        if (viewName.IsNullOrEmpty())
            throw new InvalidOperationException("View name cannot be null.");

        var partialViewName = string.Concat(partialViewPrefix, viewName);

        // check if partial exists, otherwise we'll use the same view
        var partialExists = viewResult.ViewEngineCollection.FindPartialView(controllerContext, partialViewName).View != null;

        var partialViewResult = new PartialViewResult
        {
            ViewData = viewResult.ViewData,
            TempData = viewResult.TempData,
            ViewName = partialExists ? partialViewName : viewName,
            ViewEngineCollection = viewResult.ViewEngineCollection,
        };

        return partialViewResult;
    }
}

You then need to register the formatter when your application starts:

protected void Application_Start()
{
	// ...

    ViewResultFormatters.Formatters.Add(new PartialViewResultFormatter());
}

You can checkout the code on GitHub. Feedback appreciated.

[Update]

I’ve changed the IViewResultFormatter interface to accommodate formatters than need access to the current result. The original ViewResultFormatter base class has been renamed to MediaTypeResultFormatter and I’ve updated the examples above.

© 2022 Ben Foster