JSONP з веб-API ASP.NET


136

Я працюю над створенням нового набору сервісів у ASP.MVC MVC 4 за допомогою Web API. Поки це чудово. Я створив сервіс і змусив його працювати, і тепер я намагаюся споживати його за допомогою JQuery. Я можу повернути рядок JSON за допомогою Fiddler, і, здається, це нормально, але оскільки служба існує на окремому сайті, намагаюся викликати її з помилками JQuery із пунктом "Не дозволено". Отже, це явно випадок, коли мені потрібно використовувати JSONP.

Я знаю, що веб-API новий, але сподіваюся, що хтось там може мені допомогти.

Як здійснити дзвінок до методу Web API за допомогою JSONP?


1
Я просто вивчав нову структуру веб-API після перегляду відео Скоттгу на Channel9 та прочитав статтю Скотта Хензельмана, і це було однією з моїх перших думок / питань щодо цього.
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 видалено у MVC4 RC Version forums.asp.net/post/5102318.aspx
Diganta Kumar

13
Код зараз є частиною WebApiContrib в NuGet. Не потрібно втягувати його вручну.
Джон Онстотт

7
Так, тепер просто: "Install-Package WebApiContrib.Formatting.Jsonp" Doco знаходиться тут: nuget.org/packages/WebApiContrib.Formatting.Jsonp
1313

4
Ось що мені довелося поставити, використовуючи сьогоднішнє nuget завантаження:GlobalConfiguration.Configuration.AddJsonpFormatter(config.Formatters.JsonFormatter, "callback");
joym8

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;
}

Чудово співпрацював з VS2013 U5, MVC5.2 та WebApi 2
Проконсультуйтесь з Yarla

11

Безумовно, відповідь Брайана є правильною, але якщо ви вже використовуєте формат Json.Net, який дає вам досить json дати та більш швидку серіалізацію, то ви не можете просто додати другий форматтер для jsonp, вам доведеться поєднати два. Корисно використовувати його в будь-якому випадку, оскільки Скотт Хензельман сказав, що при випуску веб-API ASP.NET використовується стандартний серіалізатор 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



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);
        }
    }

Дякую, інша версія не працює в останніх рамках .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властивість. Передача Funcв прив'язує властивість у лямбда, тому Mappingзможете отримати доступ до неї пізніше в TryMatchMediaType. Якщо у вас є більш елегантний спосіб, будь ласка, коментуйте!


2

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


1

johperl, Thomas. Відповідь, надана Пітером Мобергом вище, повинна бути правильною для 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);
        }

1

Замість розміщення власної версії формату JSONP ви можете встановити пакет WebApiContrib.Formatting.Jsonp NuGet з уже реалізованим (виберіть версію, яка працює для вашого .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);
        }

Запит. Метод дасть вам "GET", "POST" тощо, і GetQueryNameValuePairs може отримати параметр "зворотний виклик". Таким чином мій переглянений код виглядає так:

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);
    }
}

0

Ми можемо вирішити проблему CORS (перехресного походження ресурсів) двома способами,

1) Використання Jsonp 2) Увімкнення Корсів

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

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

2) Увімкнення Корсів -

щоб увімкнути диски, нам потрібно додати пакунок нульового пакета Microsoft.AspNet.WebApi.Cors і потрібно включити диски у скріншоті WebApiConfig.cs.

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

Для отримання додаткової довідки, ви можете посилатися на мій зразок репо на GitHub за наступним посиланням. https://github.com/mahesh353/Ninject.WebAPi/tree/develop

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.