JSONP с веб-интерфейсом ASP.NET
Я работаю над созданием нового набора сервисов в ASP.MVC MVC 4 с использованием веб-API. Пока это здорово. Я создал службу и получил ее на работу, и теперь я пытаюсь ее использовать с помощью JQuery. Я могу вернуть строку JSON с помощью Fiddler, и, похоже, все в порядке, но поскольку служба существует на отдельном сайте, она пытается вызвать ее с ошибками JQuery с помощью "Не разрешено". Итак, это явно случай, когда мне нужно использовать JSONP.
Я знаю, что веб-API является новым, но я надеюсь, что кто-то там может мне помочь.
Как сделать вызов метода веб-API с помощью JSONP?
Ответы
Ответ 1
После того, как я задал этот вопрос, я наконец нашел то, что мне было нужно, поэтому я отвечаю на него.
Я столкнулся с этим 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);
}
})
Кажется, что он работает очень хорошо.
Ответ 2
Вот обновленная версия 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);
}
}
Ответ 3
Вы можете использовать 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;
}
Ответ 4
Конечно, ответ Брайана правильный, однако, если вы уже используете форматировщик 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);
}
}
Ответ 5
Реализация Рика Страгла работала лучше для меня с RC.
Ответ 6
JSONP работает только с запросом Http GET. В asp.net web api есть поддержка CORS, которая хорошо работает со всеми глаголами http.
Эта статья может быть вам полезна.
Ответ 7
Обновление
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);
}
}
Ответ 8
Здесь представлена обновленная версия с несколькими улучшениями, которая работает с версией веб-API RTM.
- Выбор правильной кодировки на основе собственных заголовков
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>
во внутреннем конструкторе класса, но это был самый быстрый способ обойти проблему, которую он решает, поскольку С# имеет только статические внутренние классы, t см. свойство CallbackQueryParameter
. Передача Func
связывает свойство в лямбда, поэтому Mapping
сможет получить к нему доступ позже в TryMatchMediaType
. Если у вас более элегантный способ, прокомментируйте!
Ответ 9
johperl, Томас. Ответ, предложенный Питером Мобергом выше, должен быть правильным для версии RC как JsonMediaTypeFormatter, который он наследует от использования сериализатора NewtonSoft Json уже, и поэтому он должен работать без каких-либо изменений.
Однако, почему люди все еще используют параметры, когда вы можете просто сделать следующее
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);
}
Ответ 10
Вместо размещения собственной версии форматирования JSONP вы можете установить WebApiContrib.Formatting.Jsonp Пакет NuGet с уже реализованным (выберите версию, которая работает для вашего .NET Framework).
Добавьте этот форматтер в Application_Start
:
GlobalConfiguration.Configuration.Formatters.Insert(0, new JsonpMediaTypeFormatter(new JsonMediaTypeFormatter()));
Ответ 11
К сожалению, у меня недостаточно репутации для комментариев, поэтому я отправлю ответ. @Justin поднял вопрос о запуске WebApiContrib.Formatting.Jsonp форматирования наряду со стандартным JsonFormatter. Эта проблема решена в последней версии (фактически выпущенной некоторое время назад). Кроме того, он должен работать с последней версией веб-API.
Ответ 12
Для тех из вас, кто использует 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.
С.
Ответ 13
Проверьте это. Посмотрите, помогает ли это.
JSONP с веб-API
Ответ 14
Если контекст 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);
}
}