JSONP Formatter для ASP.NET Web API

в 5:45, , рубрики: .net, api, ASP, ASP.NET, asp.net web api, web api

ASP.NET Web API из коробки не включает в себя JSONP Formatter, но его довольно просто создать собственными руками.

Зачем нам JSONP

JSONP — это одна из возможностей javascript-приложений обойти ограничение на получение данных с сервера отличного от сервера с которого произошла загрузка приложения. JSONP оборачивает данные в формате JSON в функцию, которая выполняется при получении данных с сервера. Представим, что у нас есть ресурс, например, http://RemoteDomain/aspnetWebApi/albums, который на GET-запрос отдает нам список альбомов и он умеет отдавать этот список в формате JSONP. При использовании jQuery выглядит это так:

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

Как выглядит JSONP

JSONP довольно простой «протокол». Все что он делает — это оборачивает данные в формате JSON в функцию. Результат приведенного выше запроса выглядит примерно так:

Query17103401925975181569_1333408916499( [{"Id":"34043957","AlbumName":"Dirty Deeds Done Dirt Cheap",…},{…}] )

jQuery посылает запрос, получает ответ и «выполняет» его, принимая JSON-данные как параметр.

Как работает JSONP

Чтобы понять, как работает JSONP, приведу следующий пример на чистом 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);
}

Аналогично предыдущему примеру с jQuery, используем данную функцию для получения списка альбомов:

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

JSONP и ASP.NET Web API

Как было отмечено в начале статьи, ASP.NET Web API из коробки не поддерживает JSONP. Однако, очень просто создать собственный JSONP formatter и подключить его к проекту.

Приведенный ниже код основан на примере Christian Weyer. Код был доработан для совместимости с последней 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;
        }
    }
}

Отмечу ещё раз, что данный код не будет работать с бета-версией Web API, он работает только с версией RTM.

Также нужно отметить, что при подключении этого JSONP formatter вы фактически заменяете стоковый JSON formatter, потому что он обрабатывает те же самые MIME-типы. Этот код по прежнему использует стоковый JSON formatter, но не инициализирет его, а создает новый экземпляр для каждого JSON или JSONP запроса. Это означает, что если вам будет нужно каким-то образов настроить JSON formatter, делать это нужно будет в этом коде путем переопределения GetPerRequestFormatterInstance().

Подключение JSONP formatter

Подключение JSONP formatter происходит путем добавления его в Formatter collection в секции Application_Start() файла Global.asax.cs

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

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

Вот и все.

Примечание. Я добавил JSONP formatter перед всеми остальными. Необходимо, чтобы JSON formatter был указан до стокового JSON formatter, иначе он никогда не будет вызван.

Исходный код на GitHub

От переводчика

В целом это очень вольный перевод, поэтому все оплеухи, пожалуйста, в личку. Многие части опущены, так как для передачи сути не играли значимой роли. В любом случае прошу обращаться к оригиналу.

В Web API RTM метод WriteToStreamAsync() отличается от такового в Web API RC одним параметром: в первом HttpContent, во втором — HttpContentHeader.

Я подключал JSON Formatter в файле 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);
}

Автор: mgrach

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js