JSONP z ASP.NET Web API

136

Pracuję nad stworzeniem nowego zestawu usług w ASP.MVC MVC 4 przy użyciu Web API. Jak dotąd jest świetnie. Utworzyłem usługę i uruchomiłem ją, a teraz próbuję ją wykorzystać za pomocą JQuery. Mogę odzyskać ciąg JSON za pomocą programu Fiddler i wydaje się, że jest to w porządku, ale ponieważ usługa istnieje w oddzielnej witrynie, próbuję wywołać ją z błędami JQuery z komunikatem „Niedozwolone”. Jest to więc oczywiście przypadek, w którym muszę używać JSONP.

Wiem, że interfejs API sieci Web jest nowy, ale mam nadzieję, że ktoś tam może mi pomóc.

Jak wywołać metodę interfejsu API sieci Web przy użyciu formatu JSONP?

Brian McCord
źródło
1
Właśnie przyglądałem się nowej strukturze interfejsu API sieci Web po obejrzeniu filmu ScottGu na Channel9 i przeczytaniu artykułu Scotta Hanselmana i była to jedna z moich pierwszych myśli / pytań na ten temat.
Tracker1

Odpowiedzi:

132

Po zadaniu tego pytania w końcu znalazłem to, czego potrzebowałem, więc odpowiadam.

Natknąłem się na ten JsonpMediaTypeFormatter . Dodaj go do Application_Startswojego global.asax, wykonując następujące czynności:

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

i dobrze jest przejść z wywołaniem JQuery AJAX, które wygląda następująco:

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

Wydaje się, że działa bardzo dobrze.

Brian McCord
źródło
Wydaje się, że nie działa w moim przypadku, gdzie mam już dodany program formatujący do serializacji Json.Net. Jakieś pomysły?
Justin
4
Uważam, że FormatterContext został usunięty w wersji MVC4
Diganta Kumar
13
Kod jest teraz częścią WebApiContrib w NuGet. Nie ma potrzeby ciągnięcia go ręcznie.
Jon Onstott
7
Tak, teraz tylko: „Install-Package WebApiContrib.Formatting.Jsonp” Doco jest tutaj: nuget.org/packages/WebApiContrib.Formatting.Jsonp
nootn
4
Oto, co musiałem umieścić, korzystając z dzisiejszego pobrania GlobalConfiguration.Configuration.AddJsonpFormatter(config.Formatters.JsonFormatter, "callback");
nuget
52

Oto zaktualizowana wersja JsonpMediaTypeFormatter do użytku z 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
źródło
8
Niesamowite dzięki, chociaż uważam, że WriteToStreamAsync powinien wziąć obiekt HttpContent, a nie HttpContentHeaders, teraz w ostatecznym wydaniu, ale z tą jedną zmianą zadziałało jak urok
Ben
21

Możesz użyć ActionFilterAttribute w następujący sposób:

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

Następnie włącz to do swojej akcji:

[JsonCallback]
public IEnumerable<User> User()
{
    return _user;
}
010227leo
źródło
Doskonale działało z VS2013 U5, MVC5.2 i WebApi 2
skonsultuj się z Yarla
11

Z pewnością odpowiedź Briana jest poprawna, jednak jeśli już używasz programu formatującego Json.Net, który zapewnia ładne daty json i szybszą serializację, nie możesz po prostu dodać drugiego programu formatującego dla jsonp, musisz połączyć oba. Mimo wszystko warto go używać, ponieważ Scott Hanselman powiedział, że w wersji ASP.NET Web API domyślnie będzie używany serializator 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);
        }
    }
Justin
źródło
Jak możemy to zrobić dla RC ASP .NET Web API?
jonperl
zainteresowany również wersją RC
Thomas Stock
6

JSONP działa tylko z żądaniem HTTP GET. Istnieje obsługa CORS w interfejsie API sieci Web asp.net, który działa dobrze ze wszystkimi zleceniami http.

Ten artykuł może być dla Ciebie pomocny.

user1186065
źródło
1
Teraz jest obsługa CORS w interfejsie API sieci Web. Ten artykuł jest bardzo pomocny - asp.net/web-api/overview/security/ ...
Ilia Barahovski
5

Zaktualizowano

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
źródło
Dziękuję, druga wersja nie działa w najnowszym frameworku .net.
djbielejeski
2

Oto zaktualizowana wersja z kilkoma ulepszeniami, która działa z wersją RTM interfejsów API sieci Web.

  • Wybiera poprawne kodowanie na podstawie własnych Accept-Encodingnagłówków żądania . W new StreamWriter()poprzednich przykładach po prostu używałby UTF-8. Wywołanie do base.WriteToStreamAsyncmoże używać innego kodowania, co powoduje uszkodzenie danych wyjściowych.
  • Mapuje żądania JSONP do application/javascript Content-Typenagłówka; poprzedni przykład wyprowadziłby JSONP, ale z application/jsonnagłówkiem. Ta praca jest wykonywana w Mappingklasie zagnieżdżonej (por. Najlepszy typ treści do obsługi JSONP? )
  • Zrezygnuje z konstrukcji i opróżnienia narzutu a StreamWriteri bezpośrednio pobiera bajty i zapisuje je w strumieniu wyjściowym.
  • Zamiast czekać na zadanie, użyj ContinueWithmechanizmu biblioteki zadań równoległych, aby połączyć kilka zadań razem.

Kod:

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
}

Zdaję sobie sprawę z „zepsutości” Func<string>parametru w konstruktorze klasy wewnętrznej, ale był to najszybszy sposób obejścia problemu, który rozwiązuje - ponieważ C # ma tylko statyczne klasy wewnętrzne, nie widzi CallbackQueryParameterwłaściwości. Przekazanie Funcin wiąże właściwość w lambdzie, dzięki czemu Mappingbędzie można uzyskać do niej dostęp później TryMatchMediaType. Jeśli masz bardziej elegancki sposób, skomentuj!

atanamir
źródło
2

Niestety nie mam wystarczającej reputacji, aby komentować, więc opublikuję odpowiedź. @Justin poruszył problem uruchamiania programu formatującego WebApiContrib.Formatting.Jsonp wraz ze standardowym JsonFormatter. Ten problem został rozwiązany w najnowszej wersji (faktycznie wydanej jakiś czas temu). Powinien również działać z najnowszą wersją interfejsu API sieci Web.

panele szklane
źródło
1

Johperl, Thomas. Odpowiedź udzielona przez Petera Moberga powyżej powinna być poprawna dla wersji RC, ponieważ JsonMediaTypeFormatter, który dziedziczy z już korzysta z serializatora NewtonSoft Json, więc to, co ma, powinno działać bez żadnych zmian.

Jednak dlaczego, do licha, ludzie nadal używają naszych parametrów, skoro można po prostu wykonać następujące czynności

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
źródło
1

Zamiast hostować własną wersję programu formatującego JSONP , możesz zainstalować pakiet WebApiContrib.Formatting.Jsonp NuGet z już zaimplementowanym (wybierz wersję, która działa dla Twojego .NET Framework).

Dodaj ten formater do Application_Start:

GlobalConfiguration.Configuration.Formatters.Insert(0, new JsonpMediaTypeFormatter(new JsonMediaTypeFormatter()));
Mr. Pumpkin
źródło
0

Dla tych z Was, którzy używają HttpSelfHostServer, ta sekcja kodu zakończy się niepowodzeniem w HttpContext.Current, ponieważ nie istnieje na własnym serwerze.

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

Możesz jednak przechwycić „kontekst” hosta własnego za pomocą tego nadpisania.

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 da ci "GET", "POST" itp., A GetQueryNameValuePairs może pobrać parametr? Callback. Tak więc mój poprawiony kod wygląda następująco:

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

Mam nadzieję, że to pomoże niektórym z was. W ten sposób niekoniecznie potrzebujesz podkładki HttpContext.

DO.

Kojot
źródło
0

Jeśli kontekst brzmi Web Api, dziękując i odnosząc się do 010227leoodpowiedzi, musisz rozważyć WebContext.Currentwartość, która będzie null.

Więc zaktualizowałem jego kod do tego:

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
źródło
0

Możemy rozwiązać problem CORS (współdzielenie zasobów między źródłami) na dwa sposoby,

1) Korzystanie z Jsonp 2) Włączanie Cors

1) Używając Jsonp - aby użyć Jsonp musimy zainstalować pakiet nuget WebApiContrib.Formatting.Jsonp i musimy dodać JsonpFormmater w WebApiConfig.cs, patrz zrzuty ekranu,wprowadź opis obrazu tutaj

Kod JQuery wprowadź opis obrazu tutaj

2) Włączanie Corsa -

aby włączyć cors, musimy dodać pakiet nuget Microsoft.AspNet.WebApi.Cors i włączyć cors w WebApiConfig.cs, patrz zrzut ekranu

wprowadź opis obrazu tutaj

Aby uzyskać więcej informacji, możesz polecić moje przykładowe repozytorium na GitHub, korzystając z poniższego linku. https://github.com/mahesh353/Ninject.WebAPi/tree/develop

Mendax
źródło