JSONP с ASP.NET Web API

136

Я работаю над созданием нового набора сервисов в ASP.MVC MVC 4 с использованием Web API. Пока это здорово. Я создал сервис и заставил его работать, и теперь я пытаюсь использовать его с помощью JQuery. Я могу вернуть строку JSON с помощью Fiddler, и, похоже, все в порядке, но поскольку служба существует на отдельном сайте, я пытаюсь вызвать ее с ошибками JQuery с помощью «Not Allowed». Итак, это тот случай, когда мне нужно использовать JSONP.

Я знаю, что веб-API является новым, но я надеюсь, что кто-то может мне помочь.

Как сделать вызов метода Web API с использованием JSONP?

Брайан МакКорд
источник
1
Просто смотрел на новую структуру Web API после просмотра видео ScottGu на Channel 9 и прочитал статью Скотта Хансельмана, и это была одна из моих первых мыслей / вопросов по этому поводу.
Tracker1

Ответы:

132

Задав этот вопрос, я наконец-то нашел то, что мне было нужно, поэтому отвечаю на него.

Я столкнулся с этим 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);
    }
})

Кажется, работает очень хорошо.

Брайан МакКорд
источник
Кажется, не работает в моем случае, где у меня уже есть добавленный форматер для сериализации Json.Net. Любые идеи?
Джастин
4
Я полагаю, что FormatterContext удален в версии RC MVC4 forums.asp.net/post/5102318.aspx
Диганта Кумар
13
Код теперь является частью WebApiContrib в NuGet. Не нужно тянуть его вручную.
Джон Онстотт
7
Да, теперь просто: «Install-Package WebApiContrib.Formatting.Jsonp» Doco находится здесь: nuget.org/packages/WebApiContrib.Formatting.Jsonp
nootn
4
Это то, что я должен был положить, используя сегодняшнюю загрузку GlobalConfiguration.Configuration.AddJsonpFormatter(config.Formatters.JsonFormatter, "callback");
nuget
52

Вот обновленная версия 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);
    }
}
Питер Моберг
источник
8
Огромное спасибо, хотя я считаю, что WriteToStreamAsync должен взять HttpContent, а не объект HttpContentHeaders, теперь в финальной версии, но с этим одним изменением работало как чудо
Бен
21

Вы можете использовать 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;
}
010227leo
источник
Отлично работал с VS2013 U5, MVC5.2 и WebApi 2
проконсультируйтесь
11

Конечно, ответ Брайана правильный, но если вы уже используете форматтер 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);
        }
    }
Джастин
источник
Как мы можем сделать это для ASP .NET Web API RC?
Jonperl
Также интересует RC версия
Thomas Stock
6

JSONP работает только с HETP GET-запросом. В веб-интерфейсе asp.net есть поддержка CORS, которая хорошо работает со всеми глаголами http.

Эта статья может быть полезна для вас.

user1186065
источник
1
Теперь есть поддержка CORS в Web API. Эта статья очень полезна - asp.net/web-api/overview/security/…
Илья Бараховский
5

обновленный

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);
        }
    }
ITXGEN
источник
Спасибо, другая версия не работает в последней платформе .net.
djbielejeski
2

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

Атанамир
источник
2

К сожалению, у меня недостаточно репутации, чтобы комментировать, поэтому я выложу ответ. @Justin поднял вопрос о запуске форматера WebApiContrib.Formatting.Jsonp вместе со стандартным JsonFormatter. Эта проблема решена в последней версии (фактически выпущенной некоторое время назад). Кроме того, он должен работать с последней версией веб-API.

panesofglass
источник
1

Йохперл, Томас Ответ, данный Питером Мобергом выше, должен быть верным для 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);
        }
stevethethread
источник
1

Вместо размещения собственной версии средства форматирования JSONP вы можете установить пакет NuGet WebApiContrib.Formatting.Jsonp с уже реализованным (выберите версию, которая подходит для вашего .NET Framework).

Добавьте этот форматер в Application_Start:

GlobalConfiguration.Configuration.Formatters.Insert(0, new JsonpMediaTypeFormatter(new JsonMediaTypeFormatter()));
Мистер тыква
источник
0

Для тех из вас, кто использует 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.

койот
источник
0

Если контекст 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);
    }
}
Rikki
источник
0

Мы можем решить проблему CORS (совместного использования ресурсов из разных источников) двумя способами:

1) Использование Jsonp 2) Включение Cors

1) Используя Jsonp - для использования Jsonp нам нужно установить пакет nuget WebApiContrib.Formatting.Jsonp и добавить JsonpFormmater в WebApiConfig.cs со ссылками на скриншоты,введите описание изображения здесь

Код Jquery введите описание изображения здесь

2) Включение Cors -

Чтобы включить Cors, нам нужно добавить пакет nuget Microsoft.AspNet.WebApi.Cors, и нам нужно включить Cors в WebApiConfig.cs. Смотрите скриншот.

введите описание изображения здесь

Для получения дополнительной информации вы можете обратиться к моему примеру репозитория на GitHub, используя следующую ссылку. https://github.com/mahesh353/Ninject.WebAPi/tree/develop

Мендаксу
источник