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, иначе он никогда не будет вызван.
От переводчика
В целом это очень вольный перевод, поэтому все оплеухи, пожалуйста, в личку. Многие части опущены, так как для передачи сути не играли значимой роли. В любом случае прошу обращаться к оригиналу.
В 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