JSONP с ASP.NET Web API
Я работаю над созданием нового набора сервисов в ASP.MVC MVC 4 с использованием Web API. Пока это здорово. Я создал сервис и заставил его работать, и теперь я пытаюсь использовать его с помощью JQuery. Я могу вернуть строку JSON с помощью Fiddler, и, похоже, все в порядке, но поскольку служба существует на отдельном сайте, я пытаюсь вызвать ее с ошибками JQuery с помощью "Not Allowed". Итак, это тот случай, когда мне нужно использовать JSONP.
Я знаю, что веб-API является новым, но я надеюсь, что кто-то может мне помочь.
Как сделать вызов метода веб-API с помощью JSONP?
15 ответов
Задав этот вопрос, я наконец-то нашел то, что мне было нужно, поэтому отвечаю на него.
Я столкнулся с этим JsonpMediaTypeFormatter. Добавьте это в Application_Start
вашего global.asax, выполнив это:
var config = GlobalConfiguration.Configuration;
config.Formatters.Insert(0, new JsonpMediaTypeFormatter());
и вы можете использовать вызов JQuery AJAX, который выглядит следующим образом:
$.ajax({
url: 'http://myurl.com',
type: 'GET',
dataType: 'jsonp',
success: function (data) {
alert(data.MyProperty);
}
})
Кажется, работает очень хорошо.
Вот обновленная версия JsonpMediaTypeFormatter для использования с WebAPI RC:
public class JsonpMediaTypeFormatter : JsonMediaTypeFormatter
{
private string callbackQueryParameter;
public JsonpMediaTypeFormatter()
{
SupportedMediaTypes.Add(DefaultMediaType);
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/javascript"));
MediaTypeMappings.Add(new UriPathExtensionMapping("jsonp", DefaultMediaType));
}
public string CallbackQueryParameter
{
get { return callbackQueryParameter ?? "callback"; }
set { callbackQueryParameter = value; }
}
public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContent content, TransportContext transportContext)
{
string callback;
if (IsJsonpRequest(out callback))
{
return Task.Factory.StartNew(() =>
{
var writer = new StreamWriter(stream);
writer.Write(callback + "(");
writer.Flush();
base.WriteToStreamAsync(type, value, stream, content, transportContext).Wait();
writer.Write(")");
writer.Flush();
});
}
else
{
return base.WriteToStreamAsync(type, value, stream, content, transportContext);
}
}
private bool IsJsonpRequest(out string callback)
{
callback = null;
if (HttpContext.Current.Request.HttpMethod != "GET")
return false;
callback = HttpContext.Current.Request.QueryString[CallbackQueryParameter];
return !string.IsNullOrEmpty(callback);
}
}
Вы можете использовать ActionFilterAttribute следующим образом:
public class JsonCallbackAttribute : ActionFilterAttribute
{
private const string CallbackQueryParameter = "callback";
public override void OnActionExecuted(HttpActionExecutedContext context)
{
var callback = string.Empty;
if (IsJsonp(out callback))
{
var jsonBuilder = new StringBuilder(callback);
jsonBuilder.AppendFormat("({0})", context.Response.Content.ReadAsStringAsync().Result);
context.Response.Content = new StringContent(jsonBuilder.ToString());
}
base.OnActionExecuted(context);
}
private bool IsJsonp(out string callback)
{
callback = HttpContext.Current.Request.QueryString[CallbackQueryParameter];
return !string.IsNullOrEmpty(callback);
}
}
Тогда поместите это в свое действие:
[JsonCallback]
public IEnumerable<User> User()
{
return _user;
}
Конечно, ответ Брайана правильный, но если вы уже используете форматтер Json.Net, который дает вам красивые даты json и более быструю сериализацию, то вы не можете просто добавить второй форматер для jsonp, вам нужно объединить их. В любом случае, это хорошая идея, так как Скотт Хансельман сказал, что при выпуске ASP.NET Web API по умолчанию будет использоваться сериализатор Json.Net.
public class JsonNetFormatter : MediaTypeFormatter
{
private JsonSerializerSettings _jsonSerializerSettings;
private string callbackQueryParameter;
public JsonNetFormatter(JsonSerializerSettings jsonSerializerSettings)
{
_jsonSerializerSettings = jsonSerializerSettings ?? new JsonSerializerSettings();
// Fill out the mediatype and encoding we support
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/json"));
Encoding = new UTF8Encoding(false, true);
//we also support jsonp.
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/javascript"));
MediaTypeMappings.Add(new UriPathExtensionMapping("jsonp", "application/json"));
}
public string CallbackQueryParameter
{
get { return callbackQueryParameter ?? "jsoncallback"; }
set { callbackQueryParameter = value; }
}
protected override bool CanReadType(Type type)
{
if (type == typeof(IKeyValueModel))
return false;
return true;
}
protected override bool CanWriteType(Type type)
{
return true;
}
protected override Task<object> OnReadFromStreamAsync(Type type, Stream stream, HttpContentHeaders contentHeaders,
FormatterContext formatterContext)
{
// Create a serializer
JsonSerializer serializer = JsonSerializer.Create(_jsonSerializerSettings);
// Create task reading the content
return Task.Factory.StartNew(() =>
{
using (StreamReader streamReader = new StreamReader(stream, Encoding))
{
using (JsonTextReader jsonTextReader = new JsonTextReader(streamReader))
{
return serializer.Deserialize(jsonTextReader, type);
}
}
});
}
protected override Task OnWriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders,
FormatterContext formatterContext, TransportContext transportContext)
{
string callback;
var isJsonp = IsJsonpRequest(formatterContext.Response.RequestMessage, out callback);
// Create a serializer
JsonSerializer serializer = JsonSerializer.Create(_jsonSerializerSettings);
// Create task writing the serialized content
return Task.Factory.StartNew(() =>
{
using (JsonTextWriter jsonTextWriter = new JsonTextWriter(new StreamWriter(stream, Encoding)) { CloseOutput = false })
{
if (isJsonp)
{
jsonTextWriter.WriteRaw(callback + "(");
jsonTextWriter.Flush();
}
serializer.Serialize(jsonTextWriter, value);
jsonTextWriter.Flush();
if (isJsonp)
{
jsonTextWriter.WriteRaw(")");
jsonTextWriter.Flush();
}
}
});
}
private bool IsJsonpRequest(HttpRequestMessage request, out string callback)
{
callback = null;
if (request.Method != HttpMethod.Get)
return false;
var query = HttpUtility.ParseQueryString(request.RequestUri.Query);
callback = query[CallbackQueryParameter];
return !string.IsNullOrEmpty(callback);
}
}
JSONP работает только с HETP GET-запросом. В веб-интерфейсе asp.net есть поддержка CORS, которая хорошо работает со всеми глаголами http.
Эта статья может быть полезна для вас.
обновленный
public class JsonpMediaTypeFormatter : JsonMediaTypeFormatter
{
private string callbackQueryParameter;
public JsonpMediaTypeFormatter()
{
SupportedMediaTypes.Add(DefaultMediaType);
SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/javascript"));
MediaTypeMappings.Add(new UriPathExtensionMapping("jsonp", DefaultMediaType));
}
public string CallbackQueryParameter
{
get { return callbackQueryParameter ?? "callback"; }
set { callbackQueryParameter = value; }
}
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
{
string callback;
if (IsJsonpRequest(out callback))
{
return Task.Factory.StartNew(() =>
{
var writer = new StreamWriter(writeStream);
writer.Write(callback + "(");
writer.Flush();
base.WriteToStreamAsync(type, value, writeStream, content, transportContext).Wait();
writer.Write(")");
writer.Flush();
});
}
else
{
return base.WriteToStreamAsync(type, value, writeStream, content, transportContext);
}
}
private bool IsJsonpRequest(out string callback)
{
callback = null;
if (HttpContext.Current.Request.HttpMethod != "GET")
return false;
callback = HttpContext.Current.Request.QueryString[CallbackQueryParameter];
return !string.IsNullOrEmpty(callback);
}
}
К сожалению, у меня недостаточно репутации, чтобы комментировать, поэтому я выложу ответ. @Justin поднял вопрос о запуске форматера https://www.nuget.org/packages/WebApiContrib.Formatting.Jsonp вместе со стандартным JsonFormatter. Эта проблема решена в последней версии (фактически выпущенной некоторое время назад). Кроме того, он должен работать с последней версией веб-API.
Вот обновленная версия с несколькими улучшениями, которая работает с RTM-версией веб-API.
- Выбирает правильную кодировку, основываясь на собственном запросе
Accept-Encoding
заголовки.new StreamWriter()
в предыдущих примерах просто использовал бы UTF-8. Призыв кbase.WriteToStreamAsync
может использовать другую кодировку, что приведет к повреждению вывода. - Карты JSONP запросы к
application/javascript
Content-Type
заголовок; предыдущий пример будет выводить JSONP, но сapplication/json
заголовок. Эта работа выполняется во вложенномMapping
класс (ср. Лучший тип контента для обслуживания JSONP?) - Отказывается от строительства и промывки
StreamWriter
и напрямую получает байты и записывает их в выходной поток. - Вместо ожидания задачи используйте библиотеку параллельных задач.
ContinueWith
механизм для объединения нескольких задач вместе.
Код:
public class JsonpMediaTypeFormatter : JsonMediaTypeFormatter
{
private string _callbackQueryParameter;
public JsonpMediaTypeFormatter()
{
SupportedMediaTypes.Add(DefaultMediaType);
SupportedMediaTypes.Add(new MediaTypeHeaderValue("application/javascript"));
// need a lambda here so that it'll always get the 'live' value of CallbackQueryParameter.
MediaTypeMappings.Add(new Mapping(() => CallbackQueryParameter, "application/javascript"));
}
public string CallbackQueryParameter
{
get { return _callbackQueryParameter ?? "callback"; }
set { _callbackQueryParameter = value; }
}
public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content,
TransportContext transportContext)
{
var callback = GetCallbackName();
if (!String.IsNullOrEmpty(callback))
{
// select the correct encoding to use.
Encoding encoding = SelectCharacterEncoding(content.Headers);
// write the callback and opening paren.
return Task.Factory.StartNew(() =>
{
var bytes = encoding.GetBytes(callback + "(");
writeStream.Write(bytes, 0, bytes.Length);
})
// then we do the actual JSON serialization...
.ContinueWith(t => base.WriteToStreamAsync(type, value, writeStream, content, transportContext))
// finally, we close the parens.
.ContinueWith(t =>
{
var bytes = encoding.GetBytes(")");
writeStream.Write(bytes, 0, bytes.Length);
});
}
return base.WriteToStreamAsync(type, value, writeStream, content, transportContext);
}
private string GetCallbackName()
{
if (HttpContext.Current.Request.HttpMethod != "GET")
return null;
return HttpContext.Current.Request.QueryString[CallbackQueryParameter];
}
#region Nested type: Mapping
private class Mapping : MediaTypeMapping
{
private readonly Func<string> _param;
public Mapping(Func<string> discriminator, string mediaType)
: base(mediaType)
{
_param = discriminator;
}
public override double TryMatchMediaType(HttpRequestMessage request)
{
if (request.RequestUri.Query.Contains(_param() + "="))
return 1.0;
return 0.0;
}
}
#endregion
}
Я знаю о "хакерстве" Func<string>
параметр в конструкторе внутреннего класса, но это был самый быстрый способ обойти решаемую проблему - поскольку в C# есть только статические внутренние классы, он не может видеть CallbackQueryParameter
имущество. Проходя Func
в связывает собственность в лямбда, так Mapping
будет иметь доступ к нему позже в TryMatchMediaType
, Если у вас есть более элегантный способ, пожалуйста, прокомментируйте!
Вместо размещения собственной версии средства форматирования JSONP вы можете установить пакет NuGet http://www.nuget.org/packages/WebApiContrib.Formatting.Jsonp/ с уже реализованным (выберите версию, которая подходит для вашего.NET Framework).
Добавьте этот форматер в Application_Start
:
GlobalConfiguration.Configuration.Formatters.Insert(0, new JsonpMediaTypeFormatter(new JsonMediaTypeFormatter()));
Йохперл, Томас Ответ, данный Питером Мобергом выше, должен быть верным для версии RC, так как JsonMediaTypeFormatter, от которого он наследует, уже использует сериализатор Json NewtonSoft, и поэтому то, что у него есть, должно работать без каких-либо изменений.
Однако, почему люди все еще используют параметры, когда вы могли бы просто сделать следующее
public override Task WriteToStreamAsync(Type type, object value, Stream stream, HttpContentHeaders contentHeaders, TransportContext transportContext)
{
var isJsonpRequest = IsJsonpRequest();
if(isJsonpRequest.Item1)
{
return Task.Factory.StartNew(() =>
{
var writer = new StreamWriter(stream);
writer.Write(isJsonpRequest.Item2 + "(");
writer.Flush();
base.WriteToStreamAsync(type, value, stream, contentHeaders, transportContext).Wait();
writer.Write(")");
writer.Flush();
});
}
return base.WriteToStreamAsync(type, value, stream, contentHeaders, transportContext);
}
private Tuple<bool, string> IsJsonpRequest()
{
if(HttpContext.Current.Request.HttpMethod != "GET")
return new Tuple<bool, string>(false, null);
var callback = HttpContext.Current.Request.QueryString[CallbackQueryParameter];
return new Tuple<bool, string>(!string.IsNullOrEmpty(callback), callback);
}
Мы можем решить проблему CORS(совместного использования ресурсов из разных источников) двумя способами:
1) Использование Jsonp 2) Включение Cors
1) Используя Jsonp- для использования Jsonp нам нужно установить пакет nuget WebApiContrib.Formatting.Jsonp и добавить JsonpFormmater в WebApiConfig.cs со ссылками на скриншоты,
2) Включение Cors -
Чтобы включить Cors, нам нужно добавить пакет nuget Microsoft.AspNet.WebApi.Cors, и нам нужно включить Cors в WebApiConfig.cs. Смотрите скриншот.
Для получения дополнительной информации, вы можете обратиться к моему примеру репозитория на GitHub, используя следующую ссылку. https://github.com/mahesh353/Ninject.WebAPi/tree/develop
Для тех из вас, кто использует HttpSelfHostServer, этот раздел кода не будет работать на HttpContext.Current, так как он не существует на самом хост-сервере.
private Tuple<bool, string> IsJsonpRequest()
{
if(HttpContext.Current.Request.HttpMethod != "GET")
return new Tuple<bool, string>(false, null);
var callback = HttpContext.Current.Request.QueryString[CallbackQueryParameter];
return new Tuple<bool, string>(!string.IsNullOrEmpty(callback), callback);
}
Однако вы можете перехватить "контекст" собственного хоста через это переопределение.
public override MediaTypeFormatter GetPerRequestFormatterInstance(Type type, HttpRequestMessage request, MediaTypeHeaderValue mediaType)
{
_method = request.Method;
_callbackMethodName =
request.GetQueryNameValuePairs()
.Where(x => x.Key == CallbackQueryParameter)
.Select(x => x.Value)
.FirstOrDefault();
return base.GetPerRequestFormatterInstance(type, request, mediaType);
}
Метод request.Method выдаст вам "GET", "POST" и т. Д., А GetQueryNameValuePairs может получить параметр? Callback. Таким образом, мой пересмотренный код выглядит так:
private Tuple<bool, string> IsJsonpRequest()
{
if (_method.Method != "GET")
return new Tuple<bool, string>(false, null);
return new Tuple<bool, string>(!string.IsNullOrEmpty(_callbackMethodName), _callbackMethodName);
}
Надеюсь, это поможет некоторым из вас. Таким образом, вам не обязательно нужна прокладка HttpContext.
C.
Если контекст Web Api
, поблагодарив и ссылаясь на 010227leo
ответ, вы должны рассмотреть WebContext.Current
значение, которое будет null
,
Поэтому я обновил его код так:
public class JsonCallbackAttribute
: ActionFilterAttribute
{
private const string CallbackQueryParameter = "callback";
public override void OnActionExecuted(HttpActionExecutedContext context)
{
var callback = context.Request.GetQueryNameValuePairs().Where(item => item.Key == CallbackQueryParameter).Select(item => item.Value).SingleOrDefault();
if (!string.IsNullOrEmpty(callback))
{
var jsonBuilder = new StringBuilder(callback);
jsonBuilder.AppendFormat("({0})", context.Response.Content.ReadAsStringAsync().Result);
context.Response.Content = new StringContent(jsonBuilder.ToString());
}
base.OnActionExecuted(context);
}
}