JSONP Formatter for ASP.NET Web API

Original author: Rick Strahl
  • Transfer
The ASP.NET Web API out of the box does not include the JSONP Formatter, but it is quite simple to create it yourself.

Why do we need JSONP


JSONP is one of the capabilities of javascript applications to bypass the restriction on receiving data from a server other than the server from which the application was downloaded. JSONP wraps JSON data in a function that executes when data is received from the server. Imagine that we have a resource, for example, RemoteDomain/aspnetWebApi/albums that gives us a list of albums for a GET request and it knows how to give this list in JSONP format. When using jQuery it looks like this:

function getAlbums() {
    $.getJSON("http://remotedomain/aspnetWebApi/albums?callback=?", null,
        function (albums) {
            alert(albums.length);
        });
}

What does JSONP look like


JSONP is a fairly simple "protocol." All he does is wrap the JSON data in a function. The result of the above query looks something like this:
jQuery sends a query, receives a response and "executes" it, taking JSON data as a parameter.

How JSONP Works


To understand how JSONP works, I will give the following example in pure javascript:

function jsonp(url, callback) {
    // создание уникального идентификатора
    var id = "_" + (new Date()).getTime();

    // создание глобального обработчика
    window[id] = function (result) {
        // вызов этого обработчика
        if (callback)
            callback(result);

        // зачистка: удаление скрипта и идентификатора
        var sc = document.getElementById(id);
        sc.parentNode.removeChild(sc);
        window[id] = null;
    }

    url = url.replace("callback=?", "callback=" + id);

    // создание тега <script>, который загрузит JSONP-скрипт
    // и выполнит его, вызвав функцию window[id]
    var script = document.createElement("script");
    script.setAttribute("id", id);
    script.setAttribute("src", url);
    script.setAttribute("type", "text/javascript");
    document.body.appendChild(script);
}

Similar to the previous jQuery example, we use this function to get the list of albums:

function getAlbumsManual() {
    jsonp("http://remotedomain/aspnetWebApi/albums?callback=?",
        function (albums) {
            alert(albums.length);
        });
}

JSONP and ASP.NET Web API


As noted at the beginning of the article, ASP.NET Web API out of the box does not support JSONP. However, it is very simple to create your own JSONP formatter and connect it to the project.

The following code is based on the example Christian Weyer. The code has been refined for compatibility with the latest Web API RTM.

using System;
using System.IO;
using System.Net;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web;
using System.Net.Http;
using Newtonsoft.Json.Converters;
using System.Web.Http;

namespace Westwind.Web.WebApi
{
    /// <summary>
    /// Handles JsonP requests when requests are fired with text/javascript
    /// </summary>
    public class JsonpFormatter : JsonMediaTypeFormatter
    {                
        public JsonpFormatter()
        {
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
            SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/javascript"));
            
            JsonpParameterName = "callback";
        }

        /// <summary>
        /// Name of the query string parameter to look for
        /// the jsonp function name
        /// </summary>
        public string JsonpParameterName {get; set; }

        /// <summary>
        /// Captured name of the Jsonp function that the JSON call
        /// is wrapped in. Set in GetPerRequestFormatter Instance
        /// </summary>
        private string JsonpCallbackFunction;

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

        /// <summary>
        /// Override this method to capture the Request object
        /// </summary>
        /// <param name="type"></param>
        /// <param name="request"></param>
        /// <param name="mediaType"></param>
        /// <returns></returns>
        public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, 
                                        System.Net.Http.HttpRequestMessage request, 
                                        MediaTypeHeaderValue mediaType)
        {       
            var formatter = new JsonpFormatter() 
            {                 
                JsonpCallbackFunction = GetJsonCallbackFunction(request) 
            };

            // this doesn't work unfortunately
            //formatter.SerializerSettings = GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings;

            // You have to reapply any JSON.NET default serializer Customizations here    
            formatter.SerializerSettings.Converters.Add(new StringEnumConverter());
            formatter.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;

            return formatter;
        }
        
        public override Task WriteToStreamAsync(Type type, object value, 
                                        Stream stream, 
                                        HttpContent content, 
                                        TransportContext transportContext)
        {                                     
            if (string.IsNullOrEmpty(JsonpCallbackFunction))
                return base.WriteToStreamAsync(type, value, stream, content, transportContext);

            StreamWriter writer = null;

            // write the pre-amble
            try
            {
                writer = new StreamWriter(stream);
                writer.Write(JsonpCallbackFunction + "(");
                writer.Flush();
            }
            catch (Exception ex)
            {
                try
                {
                    if (writer != null)
                        writer.Dispose();
                }
                catch { }

                var tcs = new TaskCompletionSource<object>();
                tcs.SetException(ex);
                return tcs.Task;
            }

            return base.WriteToStreamAsync(type, value, stream, content, transportContext)
                       .ContinueWith( innerTask =>                
                            {
                                if (innerTask.Status == TaskStatus.RanToCompletion)
                                {
                                    writer.Write(")");
                                    writer.Flush();                                    
                                }

                            },TaskContinuationOptions.ExecuteSynchronously)                        
                        .ContinueWith( innerTask =>
                            {
                                writer.Dispose();
                                return innerTask;

                            },TaskContinuationOptions.ExecuteSynchronously)
                        .Unwrap();            
        }

        /// <summary>
        /// Retrieves the Jsonp Callback function
        /// from the query string
        /// </summary>
        /// <returns></returns>
        private string GetJsonCallbackFunction(HttpRequestMessage request)
        {
            if (request.Method != HttpMethod.Get)
                return null;

            var query = HttpUtility.ParseQueryString(request.RequestUri.Query);
            var queryVal = query[this.JsonpParameterName];

            if (string.IsNullOrEmpty(queryVal))
                return null;

            return queryVal;
        }
    }
}

I note once again that this code will not work with the beta version of the Web API; it only works with the RTM version.

It should also be noted that when you connect this JSONP formatter, you actually replace the stock JSON formatter, because it processes the same MIME types. This code still uses the stock JSON formatter, but does not initialize it, but creates a new instance for each JSON or JSONP request. This means that if you need to configure the JSON formatter for some images, you will need to do this in this code by overriding it GetPerRequestFormatterInstance() .

JSONP formatter connection


JSONP formatter is connected by adding it to the Formatter collection in the section of Application_Start() the Global.asax.cs file

protected void Application_Start(object sender, EventArgs e)
{
    // ваш код

    GlobalConfiguration
        .Configuration
        .Formatters
        .Insert(0, new Westwind.Web.WebApi.JsonpFormatter());
}

That's all.

Note. I added a JSONP formatter in front of everyone else. It is necessary that the JSON formatter be specified before the stock JSON formatter, otherwise it will never be called.

GitHub source code

From translator


In general, this is a very free translation, so all the slap in the ears, please, in PM. Many parts are omitted, since they did not play a significant role in conveying the essence. In any case, please refer to the original.

In Web API RTM, the method WriteToStreamAsync() differs from that in Web API RC in one parameter: in the first HttpContent , in the second - HttpContentHeader .

I included JSON Formatter in the file ApiConfig.cs :
ApiConfig.cs

public static void RegisterApiConfigure(HttpConfiguration config)
{
    // Remove the JSON formatter
    //config.Formatters.Remove(config.Formatters.JsonFormatter);

    // Remove the XML formatter
    config.Formatters.Remove(config.Formatters.XmlFormatter);

    // Indenting
    //config.Formatters.JsonFormatter.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;

    // Add a custom JsopFormatter
    config.Formatters.Insert(0, new JsonpFormatter());
}

Global.asax.cs

protected void Application_Start()
{
    AreaRegistration.RegisterAllAreas();

    FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
    BundleConfig.RegisterBundles(BundleTable.Bundles);
    ApiConfig.RegisterApiConfigure(GlobalConfiguration.Configuration);
}