Вот обновленная версия с несколькими улучшениями, которая работает с 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
.Если у вас есть более элегантный способ, пожалуйста, прокомментируйте!