Enabling JSONP calls on ASP.NET MVC

JSONP is the technique of making cross-domain HTTP requests via JavaScript circumventing browser security restictions on requests that return data in the JSON format. By cross-domain, we refer to the practice of making HTTP requests on URLs that refer to resources residing on a domain other than the one from where the page being viewed/executed was loaded from. You'll find a good description of what JSONP is all about here. Briefly, the technique exploits a browser back-door where the SRC attribute on a SCRIPT tag is allowed to be a "foreign" URL and the browser will download the code and evaluate it. But then again, this is perhaps not so much a back-door as a feature that allows you to build mash-ups by composing code that draws on functionality hosted on different servers. If this were not allowed then something like the Content Delivery Networks (CDN) would just not work. In fact this blog that you're reading right now loads the jQuery JavaScript library via the Google CDN.

Here's an example of this in practice - I've loaded up the 5 most "interesting" photos uploaded on Flickr in the last day or so below:

And here's the code that accomplishes this with a little jQuery magic:

//
// Flickr REST url
//
var url = "https://api.flickr.com/services/rest/?";

//
// My Flickr API key
//
var api_key = "<<your flickr api key here>>";

//
// build a flicker url from a photo object
//
function buildPhotoUrl(photo) {
    return "http://farm" + photo.farm +
           ".static.flickr.com/" + photo.server + "/" +
           photo.id + "_" + photo.secret + "_t.jpg";
}

//
// get interesting photos
//
function getInterestingPhotos() {
    //
    // build the URL
    //
    var call = url +
               "method=flickr.interestingness.getList&api_key=" +
               api_key +
               "&per_page=5&page=1&format=json&jsoncallback=?";

    //
    // make the ajax call
    //
    $.getJSON(call, function(rsp) {
        if (rsp.stat != "ok") {
            //
            // something went wrong!
            //
            $("#interesting_photos").append(
                "<label style=\"color:red\">Whoops!  It didn't" +
                " work! This is embarrassing! Here's what " +
                "Flickr had to say about this - " + rsp.message +
                "</label>");
        }
        else {
            //
            // build the html
            //
            var html = "";
            $.each(rsp.photos.photo, function() {
                var photo = this;
                html += "<span><img src=\"" + buildPhotoUrl(photo) +
                        "\" title=\"" + photo.title + "\" alt=\"" +
                        photo.title + "\" /></span> ";
            });

            //
            // append this to the div
            //
            $("#interesting_photos").append(html);
        }
    });
}

//
// get the photos
//
$(getInterestingPhotos);

The basic idea is to dynamically add a SCRIPT tag to the DOM where the SRC attribute is pointing to the external URL where the data that we want resides. Upon encountering a new SCRIPT node, the browser immediately starts loading the code and evaluating it (and also freezes pretty much everything else in the browser while it is doing this!). If we can have the external resource render a call to a function that we define (or to a well-known function name) in the script that it produces then we can effectively have a callback routine invoked when the data is received from the foreign domain. The function to be called when the script dynamically loads is usually passed in as a query string parameter in the GET request or can be a function name that is mandated by the 3rd party site. Flickr for example defaults to rendering a call to a function called jsonFlickrApi but allows you to override this by passing a different name via the jsoncallback query string parameter.

jQuery has direct support for JSONP in that if you include a callback=? parameter in the AJAX URL for the getJSON call then it will replace the ? with a dynamically generated global JavaScript function name that it also defines. The word callback can be replaced with anything else as we did above by using jsoncallback. All the grunt work of dynamically adding SCRIPT tags to the DOM is done by jQuery.

Now, imagine that you designed your own little REST based service using ASP.NET MVC and have opted to provide JSON as one of the data output formats and wish to support JSONP requests. This basically means that you simply need to wrap your JSON output in a call to a user-supplied or a standard JavaScript function. So instead of rendering something like this out to the client:

{
    photos : [
        photo : {
            id : 232992,
            secret : "bAC980980c09c08a0ef"
        }
    ],
    page : 1,
    total : 500,
    photosPerPage : 10
}

You want to render something like this:

callback({
    photos : [
        photo : {
            id : 232992,
            secret : "bAC980980c09c08a0ef"
        }
    ],
    page : 1,
    total : 500,
    photosPerPage : 10
});

Where callback is the name of the JavaScript function defined by the client that needs to be invoked when the script is evaluated by the browser. If you wanted your ASP.NET MVC controllers to support this, how would you do it? The first approach I took was to simply define a custom ActionResult class that would produce the correct script. Here's what this looks like:

/// <summary>
/// Renders result as JSON and also wraps the JSON in a call
/// to the callback function specified in "JsonpResult.Callback".
/// </summary>
public class JsonpResult : JsonResult
{
    /// <summary>
    /// Gets or sets the javascript callback function that is
    /// to be invoked in the resulting script output.
    /// </summary>
    /// <value>The callback function name.</value>
    public string Callback { get; set; }

    /// <summary>
    /// Enables processing of the result of an action method by a
    /// custom type that inherits from
    /// <see cref="T:System.Web.Mvc.ActionResult"/>.
    /// </summary>
    /// <param name="context">The context within which the
    /// result is executed.</param>
    public override void ExecuteResult(ControllerContext context)
    {
        if (context == null)
            throw new ArgumentNullException("context");

        HttpResponseBase response = context.HttpContext.Response;
        if (!String.IsNullOrEmpty(ContentType))
            response.ContentType = ContentType;
        else
            response.ContentType = "application/javascript";

        if (ContentEncoding != null)
            response.ContentEncoding = ContentEncoding;

        if (Callback == null || Callback.Length == 0)
        {
            Callback = context.HttpContext.
              Request.QueryString["callback"];
        }

        if (Data != null)
        {
            // The JavaScriptSerializer type was marked as obsolete
            // prior to .NET Framework 3.5 SP1 
#pragma warning disable 0618
            JavaScriptSerializer serializer = 
                 new JavaScriptSerializer();
            string ser = serializer.Serialize(Data);
            response.Write(Callback + "(" + ser + ");");
#pragma warning restore 0618
        }
    }
}

Now you can simply return a JsonpResult (note the p before the "R" in "Result") from your action methods instead of JsonResult and everything should just work. Client's would indicate that this is a JSONP request by appending a query string parameter called callback to the request with the name of a JavaScript function that is to be called. I wasn't however, entirely happy with this situation as this would mean that I'd have to go and change the return type of all my action methods to return a JsonpResult instead of a JsonResult. And also update the code that instantiated JsonResult. I wanted a less disruptive solution if you will.

It is precisely for scenarios such as this that the ASP.NET MVC framework includes extension points in the form of action filters. Action filters are basically hooks that you can define to intercept and enhance request processing in various ways. ASP.NET MVC allows you to define filters that get called before and/or after the action method is invoked and also before and/or after the ExecuteResult method on the ActionResult is invoked. Action filters are defined as .NET custom attribute classes that inherit from ActionFilterAttribute. ActionFilterAttribute defines 4 virtual methods that you can override to hook into the request processing pipeline at the right juncture. The OnActionExecuted method for instance is invoked immediately after the action method has been invoked and gives you a chance to further process the returned result. This would perfectly suit our purpose here where we wish to conditionally supplant the JsonResult object being returned from the action method with a JsonpResult instead. Here's what I came up with:

public class JsonpFilterAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(
            ActionExecutedContext filterContext)
    {
        if(filterContext == null)
            throw new ArgumentNullException("filterContext");

        //
        // see if this request included a "callback" querystring
        // parameter
        //
        string callback = filterContext.HttpContext.
                  Request.QueryString["callback"];
        if (callback != null && callback.Length > 0)
        {
            //
            // ensure that the result is a "JsonResult"
            //
            JsonResult result = filterContext.Result as JsonResult;
            if (result == null)
            {
                throw new InvalidOperationException(
                    "JsonpFilterAttribute must be applied only " +
                    "on controllers and actions that return a " +
                    "JsonResult object.");
            }

            filterContext.Result = new JsonpResult
            {
                ContentEncoding = result.ContentEncoding,
                ContentType = result.ContentType,
                Data = result.Data,
                Callback = callback
            };
        }
    }
}

As is perhaps self-evident, we simply check if the query string included a parameter called callback and if it did, then we supplant the Result object in filterContext with a new JsonpResult instance. This approach allows us to enable JSONP processing on a controller or an action method by simply tagging on the JsonpFilter attribute. If I wanted all of my methods in the controller to support JSONP then, I can do this:

[JsonpFilter]
public class DoofusController : Controller
{
    //
    // all your action methods here
    //
}

Or if I wanted it to work with only a specific action method then I'd do this:

public class DoofusController : Controller
{
    [JsonpFilter]
    public JsonResult DoTheThing(string data, string moreData)
    {
        return new JsonResult
        {
            Data = FetchSomeData(data, moreData)
        };
    }
}

ASP.NET MVC is kind of cool eh?! :-P

comments powered by Disqus