JSONP con API Web ASP.NET


136

Sto lavorando alla creazione di un nuovo set di servizi in ASP.MVC MVC 4 utilizzando l'API Web. Finora è fantastico. Ho creato il servizio e l'ho fatto funzionare, e ora sto provando a consumarlo usando JQuery. Posso recuperare la stringa JSON usando Fiddler, e sembra essere ok, ma perché il servizio esiste su un sito separato, provando a chiamarlo con errori JQuery con "Non consentito". Quindi, questo è chiaramente un caso in cui ho bisogno di usare JSONP.

So che l'API Web è nuova, ma spero che qualcuno là fuori possa aiutarmi.

Come si effettua una chiamata a un metodo API Web utilizzando JSONP?


1
Stavo solo esaminando la nuova struttura dell'API Web dopo aver visto il video di ScottGu su Channel9 e aver letto l'articolo di Scott Hanselman, e questa è stata una delle mie prime riflessioni / domande su questo.
Tracker1,

Risposte:


132

Dopo aver posto questa domanda, ho finalmente trovato ciò di cui avevo bisogno, quindi sto rispondendo.

Mi sono imbattuto in questo JsonpMediaTypeFormatter . Aggiungilo al Application_Starttuo global.asax in questo modo:

var config = GlobalConfiguration.Configuration;
config.Formatters.Insert(0, new JsonpMediaTypeFormatter());

e sei bravo ad andare con una chiamata JQuery AJAX che assomiglia a questo:

$.ajax({
    url: 'http://myurl.com',
    type: 'GET',
    dataType: 'jsonp',
    success: function (data) {
        alert(data.MyProperty);
    }
})

Sembra funzionare molto bene.


Non sembra funzionare nel mio caso, in cui ho già aggiunto un formattatore per la serializzazione Json.Net. Qualche idea?
Giustino,

4
Credo che FormatterContext sia stato rimosso nella versione RC di MVC4 forums.asp.net/post/5102318.aspx
Diganta Kumar,

13
Il codice ora fa parte di WebApiContrib in NuGet. Non è necessario inserirlo manualmente.
Jon Onstott,

7
Sì, ora solo: "Installa pacchetto WebApiContrib.Formatting.Jsonp" Doco è qui: nuget.org/packages/WebApiContrib.Formatting.Jsonp
nootn

4
Questo è ciò che ho dovuto mettere usando il download di nuget di oggi:GlobalConfiguration.Configuration.AddJsonpFormatter(config.Formatters.JsonFormatter, "callback");
joym8,

52

Ecco una versione aggiornata di JsonpMediaTypeFormatter da utilizzare con 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
Fantastico grazie, anche se credo che WriteToStreamAsync dovrebbe prendere un oggetto HttpContent non un HttpContentHeaders ora nella versione finale, ma con quello un cambiamento ha funzionato come un fascino
Ben

21

È possibile utilizzare un ActionFilterAttribute in questo modo:

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

Quindi mettilo in azione:

[JsonCallback]
public IEnumerable<User> User()
{
    return _user;
}

Ha funzionato perfettamente con VS2013 U5, MVC5.2 e WebApi 2
Consultare Yarla il

11

Certamente la risposta di Brian è quella corretta, tuttavia se stai già utilizzando il formattatore Json.Net, che ti dà date piuttosto json e una serializzazione più veloce, quindi non puoi semplicemente aggiungere un secondo formattatore per jsonp, devi combinare i due. È una buona idea usarlo comunque, poiché Scott Hanselman ha affermato che il rilascio dell'API Web ASP.NET utilizzerà il serializzatore Json.Net per impostazione predefinita.

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

Come possiamo farlo per l'API Web ASP ASP RC?
jonperl,

interessato anche alla versione RC
Thomas Stock



5

aggiornato

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

Grazie, l'altra versione non funziona nell'ultimo framework .net.
djbielejeski,

2

Ecco una versione aggiornata con diversi miglioramenti, che funziona con la versione RTM delle API Web.

  • Seleziona la codifica corretta, in base alle Accept-Encodingintestazioni della richiesta . Il new StreamWriter()negli esempi precedenti sarebbe sufficiente utilizzare UTF-8. La chiamata a base.WriteToStreamAsyncpuò utilizzare una codifica diversa, con conseguente output danneggiato.
  • Mappa le richieste JSONP all'intestazione application/javascript Content-Type; l'esempio precedente avrebbe prodotto JSONP, ma con l' application/jsonintestazione. Questo lavoro viene svolto nella Mappingclasse nidificata (cfr. Il miglior tipo di contenuto per servire JSONP? )
  • Prevede il sovraccarico di costruzione e flushing di un StreamWritere ottiene direttamente i byte e li scrive nel flusso di output.
  • Invece di attendere un'attività, utilizzare il ContinueWithmeccanismo della Libreria parallela attività per concatenare più attività insieme.

Codice:

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
}

Sono consapevole della "pirateria" del Func<string>parametro nel costruttore della classe interna, ma è stato il modo più veloce per aggirare il problema che risolve - poiché C # ha solo classi interne statiche, non può vedere la CallbackQueryParameterproprietà. Il passaggio di Funcin vincola la proprietà in lambda, quindi Mappingsarà possibile accedervi in ​​seguito TryMatchMediaType. Se hai un modo più elegante, per favore commenta!


2

Sfortunatamente, non ho abbastanza reputazione per commentare, quindi posterò una risposta. @Justin ha sollevato il problema dell'esecuzione del formattatore WebApiContrib.Formatting.Jsonp insieme allo standard JsonFormatter. Tale problema è stato risolto nell'ultima versione (effettivamente rilasciata qualche tempo fa). Inoltre, dovrebbe funzionare con l'ultima versione dell'API Web.


1

johperl, Thomas. La risposta data da Peter Moberg sopra dovrebbe essere corretta per la versione RC in quanto JsonMediaTypeFormatter da cui eredita utilizza già il serializzatore Newton Joft e quindi ciò che ha dovrebbe funzionare senza modifiche.

Tuttavia, perché mai le persone usano ancora i parametri, quando potresti semplicemente fare quanto segue

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

Invece di ospitare la tua versione del formattatore JSONP puoi installare il pacchetto NuGet WebApiContrib.Formatting.Jsonp con uno già implementato (scegli la versione che funziona per il tuo .NET Framework).

Aggiungi questo formattatore in Application_Start:

GlobalConfiguration.Configuration.Formatters.Insert(0, new JsonpMediaTypeFormatter(new JsonMediaTypeFormatter()));

0

Per quelli di voi che utilizzano HttpSelfHostServer questa sezione di codice non funzionerà su HttpContext.Current, poiché non esiste sul server self host.

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

Tuttavia, è possibile intercettare il "contesto" self host tramite questa sostituzione.

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

Il request.Method ti darà "GET", "POST", ecc. E GetQueryNameValuePairs può recuperare il parametro? Callback. Quindi il mio codice modificato appare come:

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

Spero che questo aiuti alcuni di voi. In questo modo non è necessario necessariamente uno spessore di HttpContext.

C.



0

Se il contesto è Web Api, ringraziando e facendo riferimento alla 010227leorisposta, è necessario considerare il WebContext.Currentvalore che sarà null.

Quindi ho aggiornato il suo codice a questo:

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

Siamo in grado di risolvere il problema CORS (condivisione delle risorse tra le origini) in due modi,

1) Utilizzo di Jsonp 2) Abilitazione del Cors

1) Usando Jsonp- per usare Jsonp dobbiamo installare il pacchetto nuget WebApiContrib.Formatting.Jsonp e aggiungere JsonpFormmater in WebApiConfig.cs fare riferimento a screenshot,inserisci qui la descrizione dell'immagine

Codice jquery inserisci qui la descrizione dell'immagine

2) Abilitazione del Cors -

per abilitare il cors è necessario aggiungere il pacchetto nuget Microsoft.AspNet.WebApi.Cors e abilitare cors in WebApiConfig.cs fare riferimento allo screenshot

inserisci qui la descrizione dell'immagine

Per ulteriori riferimenti, puoi fare riferimento al mio repository di esempio su GitHub utilizzando il seguente link. https://github.com/mahesh353/Ninject.WebAPi/tree/develop

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.