JSONP mit ASP.NET-Web-API

136

Ich arbeite daran, mithilfe der Web-API einen neuen Satz von Diensten in ASP.MVC MVC 4 zu erstellen. Bisher ist es großartig. Ich habe den Dienst erstellt und zum Laufen gebracht, und jetzt versuche ich, ihn mit JQuery zu nutzen. Ich kann die JSON-Zeichenfolge mit Fiddler zurückerhalten, und es scheint in Ordnung zu sein, aber da der Dienst auf einer separaten Site vorhanden ist, wird versucht, ihn mit JQuery-Fehlern mit "Nicht zulässig" aufzurufen. Dies ist also eindeutig ein Fall, in dem ich JSONP verwenden muss.

Ich weiß, dass die Web-API neu ist, aber ich hoffe, dass mir jemand da draußen helfen kann.

Wie rufe ich eine Web-API-Methode mit JSONP auf?

Brian McCord
quelle
1
Ich habe mir gerade die neue Web-API-Struktur angesehen, nachdem ich mir das ScottGu-Video auf Channel9 angesehen und den Artikel von Scott Hanselman gelesen hatte, und dies war einer meiner ersten Gedanken / Fragen dazu.
Tracker1

Antworten:

132

Nachdem ich diese Frage gestellt hatte, fand ich endlich, was ich brauchte, also beantworte ich sie.

Ich bin auf diesen JsonpMediaTypeFormatter gestoßen . Fügen Sie es folgendermaßen in die Application_StartDatei global.asax ein:

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

und Sie können mit einem JQuery AJAX-Aufruf beginnen, der so aussieht:

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

Es scheint sehr gut zu funktionieren.

Brian McCord
quelle
Scheint in meinem Fall nicht zu funktionieren, in dem ich bereits einen Formatierer für die Json.Net-Serialisierung hinzugefügt habe. Irgendwelche Ideen?
Justin
4
Ich glaube, FormatterContext wird in MVC4 RC Version entfernt forums.asp.net/post/5102318.aspx
Diganta Kumar
13
Der Code ist jetzt Teil von WebApiContrib in NuGet. Sie müssen es nicht manuell einziehen.
Jon Onstott
7
Ja, jetzt nur: "Install-Package WebApiContrib.Formatting.Jsonp" Doco ist hier: nuget.org/packages/WebApiContrib.Formatting.Jsonp
nootn
4
Dies ist, was ich mit dem heutigen Nuget-Download setzen musste:GlobalConfiguration.Configuration.AddJsonpFormatter(config.Formatters.JsonFormatter, "callback");
joym8
52

Hier ist eine aktualisierte Version des JsonpMediaTypeFormatter zur Verwendung mit 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);
    }
}
Peter Moberg
quelle
8
Ein großartiger Dank, obwohl ich glaube, dass WriteToStreamAsync jetzt in der endgültigen Version ein HttpContent-Objekt und kein HttpContentHeaders-Objekt verwenden sollte, aber mit dieser einen Änderung funktionierte es wie ein Zauber
Ben
21

Sie können ein ActionFilterAttribute wie folgt verwenden:

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

Dann setzen Sie es auf Ihre Aktion:

[JsonCallback]
public IEnumerable<User> User()
{
    return _user;
}
010227leo
quelle
Funktionierte perfekt mit VS2013 U5, MVC5.2 und WebApi 2
Wenden Sie sich
11

Sicherlich ist Brians Antwort die richtige. Wenn Sie jedoch bereits den Json.Net-Formatierer verwenden, der Ihnen hübsche JSON-Daten und eine schnellere Serialisierung bietet, können Sie nicht einfach einen zweiten Formatierer für JSONP hinzufügen, sondern beide kombinieren. Es ist trotzdem eine gute Idee, es zu verwenden, da Scott Hanselman gesagt hat, dass bei der Veröffentlichung der ASP.NET-Web-API standardmäßig der Json.Net-Serializer verwendet wird.

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);
        }
    }
Justin
quelle
Wie können wir dies für die ASP .NET Web API RC tun?
Jonperl
auch interessiert an RC-Version
Thomas Stock
6

JSONP funktioniert nur mit HTTP-GET-Anforderungen. Es gibt eine CORS-Unterstützung in der asp.net-Web-API, die mit allen http-Verben gut funktioniert.

Dieser Artikel kann für Sie hilfreich sein.

user1186065
quelle
1
Jetzt gibt es CORS-Unterstützung in der Web-API. Dieser Artikel ist ziemlich hilfreich - asp.net/web-api/overview/security/…
Ilia Barahovski
5

Aktualisiert

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
quelle
Vielen Dank, die andere Version funktioniert nicht im neuesten .net-Framework.
Djbielejeski
2

Hier ist eine aktualisierte Version mit mehreren Verbesserungen, die mit der RTM-Version von Web-APIs funktioniert.

  • Wählt die richtige Codierung basierend auf den eigenen Accept-EncodingHeadern der Anforderung aus . Die new StreamWriter()in den vorherigen Beispielen würden einfach UTF-8 verwenden. Der Aufruf von base.WriteToStreamAsynckann eine andere Codierung verwenden, was zu einer beschädigten Ausgabe führt.
  • Ordnet JSONP-Anforderungen dem application/javascript Content-TypeHeader zu. Das vorherige Beispiel würde JSONP ausgeben, jedoch mit dem application/jsonHeader. Diese Arbeit wird in der verschachtelten MappingKlasse ausgeführt (vgl. Bester Inhaltstyp für JSONP? )
  • Verzichtet auf den Aufbau und das Löschen von a StreamWriterund ruft die Bytes direkt ab und schreibt sie in den Ausgabestream.
  • Verwenden Sie den ContinueWithMechanismus der Task Parallel Library , um mehrere Aufgaben miteinander zu verketten, anstatt auf eine Aufgabe zu warten .

Code:

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
}

Ich bin mir der "Hackiness" des Func<string>Parameters im Konstruktor der inneren Klasse bewusst , aber es war der schnellste Weg, um das Problem zu umgehen , das es löst - da C # nur statische innere Klassen hat, kann es die CallbackQueryParameterEigenschaft nicht sehen . Durch das Übergeben des FuncIn wird die Eigenschaft im Lambda gebunden, sodass MappingSie später in darauf zugreifen können TryMatchMediaType. Wenn Sie eine elegantere Art haben, kommentieren Sie bitte!

Atanamir
quelle
2

Leider habe ich nicht genug Ruf, um einen Kommentar abzugeben, daher werde ich eine Antwort posten. @Justin hat das Problem angesprochen , den WebApiContrib.Formatting.Jsonp- Formatierer neben dem Standard-JsonFormatter auszuführen. Dieses Problem wurde in der neuesten Version behoben (die tatsächlich vor einiger Zeit veröffentlicht wurde). Außerdem sollte es mit der neuesten Web-API-Version funktionieren.

Glasscheiben
quelle
1

Johperl, Thomas. Die Antwort von Peter Moberg oben sollte für die RC-Version korrekt sein, da der JsonMediaTypeFormatter, von dem er erbt, bereits den NewtonSoft Json-Serializer verwendet, und daher sollte das, was er hat, ohne Änderungen funktionieren.

Warum um alles in der Welt verwenden die Menschen jedoch immer noch Parameter, wenn Sie nur Folgendes tun könnten?

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
quelle
1

Anstatt Ihre eigene JSONP-Formatierungsversion zu hosten, können Sie das WebApiContrib.Formatting.Jsonp NuGet-Paket mit einer bereits implementierten installieren (wählen Sie die Version aus, die für Ihr .NET Framework funktioniert).

Fügen Sie diesen Formatierer hinzu in Application_Start:

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

Für diejenigen unter Ihnen, die den HttpSelfHostServer verwenden, schlägt dieser Codeabschnitt in HttpContext.Current fehl, da er auf dem Self-Host-Server nicht vorhanden ist.

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

Sie können jedoch den Selbstkontext des Selbsthosts über diese Überschreibung abfangen.

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

Die request.Method gibt Ihnen "GET", "POST" usw. und die GetQueryNameValuePairs können den Parameter? Callback abrufen. So sieht mein überarbeiteter Code aus:

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

Hoffe das hilft einigen von euch. Auf diese Weise benötigen Sie nicht unbedingt einen HttpContext-Shim.

C.

Kojote
quelle
0

Wenn der Kontext lautet Web Api, sich zu bedanken und auf die 010227leoAntwort zu verweisen , müssen Sie den WebContext.CurrentWert berücksichtigen , der sein wird null.

Also habe ich seinen Code folgendermaßen aktualisiert:

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
quelle
0

Wir können das CORS-Problem (Cross-Origin Resource Sharing) auf zwei Arten lösen:

1) Verwenden von Jsonp 2) Aktivieren der Cors

1) Mit Jsonp- zur Verwendung von Jsonp müssen wir das Nuget-Paket WebApiContrib.Formatting.Jsonp installieren und JsonpFormmater in WebApiConfig.cs hinzufügen.Geben Sie hier die Bildbeschreibung ein

JQuery-Code Geben Sie hier die Bildbeschreibung ein

2) Aktivieren der Cors -

Um die cors zu aktivieren, müssen wir das Microsoft.AspNet.WebApi.Cors-Nuget-Paket hinzufügen und cors in WebApiConfig.cs aktivieren. Siehe Screenshot

Geben Sie hier die Bildbeschreibung ein

Weitere Informationen finden Sie in meinem Beispiel-Repo auf GitHub unter folgendem Link. https://github.com/mahesh353/Ninject.WebAPi/tree/develop

Mendax
quelle