JSONP avec l'API Web ASP.NET

136

Je travaille sur la création d'un nouvel ensemble de services dans ASP.MVC MVC 4 à l'aide de l'API Web. Jusqu'à présent, c'est génial. J'ai créé le service et je l'ai fait fonctionner, et maintenant j'essaye de le consommer en utilisant JQuery. Je peux récupérer la chaîne JSON en utilisant Fiddler, et cela semble être correct, mais parce que le service existe sur un site séparé, en essayant de l'appeler avec des erreurs JQuery avec le "Not Allowed". Donc, c'est clairement un cas où je dois utiliser JSONP.

Je sais que l'API Web est nouvelle, mais j'espère que quelqu'un pourra m'aider.

Comment appeler une méthode API Web à l'aide de JSONP?

Brian McCord
la source
1
J'examinais juste la nouvelle structure de l'API Web après avoir regardé la vidéo ScottGu sur Channel9 et lu l'article de Scott Hanselman, et c'était l'une de mes premières réflexions / questions à ce sujet.
Tracker1

Réponses:

132

Après avoir posé cette question, j'ai finalement trouvé ce dont j'avais besoin, alors j'y réponds.

Je suis tombé sur ce JsonpMediaTypeFormatter . Ajoutez-le dans le Application_Startde votre global.asax en faisant ceci:

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

et vous êtes prêt à utiliser un appel JQuery AJAX qui ressemble à ceci:

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

Cela semble fonctionner très bien.

Brian McCord
la source
Cela ne semble pas fonctionner dans mon cas, où j'ai déjà un formateur ajouté pour la sérialisation Json.Net. Des idées?
Justin
4
Je crois que FormatterContext est supprimé dans MVC4 RC Version forums.asp.net/post/5102318.aspx
Diganta Kumar
13
Le code fait maintenant partie de WebApiContrib dans NuGet. Pas besoin de le tirer manuellement.
Jon Onstott
7
Oui, maintenant juste: "Install-Package WebApiContrib.Formatting.Jsonp" Doco est ici: nuget.org/packages/WebApiContrib.Formatting.Jsonp
nootn
4
Voici ce que j'ai dû mettre en utilisant le téléchargement du nuget d'aujourd'hui:GlobalConfiguration.Configuration.AddJsonpFormatter(config.Formatters.JsonFormatter, "callback");
joym8
52

Voici une version mise à jour de JsonpMediaTypeFormatter à utiliser avec 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
la source
8
Génial merci, bien que je pense que WriteToStreamAsync devrait prendre un HttpContent pas un objet HttpContentHeaders maintenant dans la version finale, mais avec ce changement a fonctionné comme un charme
Ben
21

Vous pouvez utiliser un ActionFilterAttribute comme ceci:

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

Ensuite, mettez-le sur votre action:

[JsonCallback]
public IEnumerable<User> User()
{
    return _user;
}
010227leo
la source
A parfaitement fonctionné avec VS2013 U5, MVC5.2 et WebApi 2
Consultez Yarla
11

La réponse de Brian est certainement la bonne, mais si vous utilisez déjà le formateur Json.Net, qui vous donne de jolies dates json et une sérialisation plus rapide, vous ne pouvez pas simplement ajouter un deuxième formateur pour jsonp, vous devez combiner les deux. C'est une bonne idée de l'utiliser quand même, car Scott Hanselman a déclaré que la version de l'API Web ASP.NET utilisera le sérialiseur Json.Net par défaut.

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
la source
Comment pouvons-nous faire cela pour l'API Web ASP .NET RC?
jonperl
également intéressé par la version RC
Thomas Stock
6

JSONP fonctionne uniquement avec la requête Http GET. Il existe un support CORS dans l'API web asp.net qui fonctionne bien avec tous les verbes http.

Cet article peut vous être utile.

user1186065
la source
1
Il existe désormais un support CORS dans l'API Web. Cet article est très utile - asp.net/web-api/overview/security/…
Ilia Barahovski
5

Actualisé

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
la source
Merci, l'autre version ne fonctionne pas dans le dernier framework .net.
djbielejeski
2

Voici une version mise à jour avec plusieurs améliorations, qui fonctionne avec la version RTM des API Web.

  • Sélectionne le codage correct, en fonction des propres en- Accept-Encodingtêtes de la demande . Le new StreamWriter()dans les exemples précédents utiliserait simplement UTF-8. L'appel à base.WriteToStreamAsyncpeut utiliser un codage différent, ce qui entraîne une sortie corrompue.
  • Mappe les demandes JSONP à l'en- application/javascript Content-Typetête; l'exemple précédent afficherait JSONP, mais avec l'en- application/jsontête. Ce travail se fait dans la Mappingclasse imbriquée (cf. Meilleur type de contenu pour servir JSONP? )
  • Renonce à la construction et au vidage de la surcharge de a StreamWriteret récupère directement les octets et les écrit dans le flux de sortie.
  • Au lieu d'attendre une tâche, utilisez le ContinueWithmécanisme de la bibliothèque parallèle de tâches pour enchaîner plusieurs tâches ensemble.

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
}

Je suis conscient du "piratage" du Func<string>paramètre dans le constructeur de classe interne, mais c'était le moyen le plus rapide de contourner le problème qu'il résout - puisque C # n'a que des classes internes statiques, il ne peut pas voir la CallbackQueryParameterpropriété. Le fait de transmettre Funcin lie la propriété dans le lambda, Mappingvous pourrez donc y accéder plus tard dans TryMatchMediaType. Si vous avez une manière plus élégante, veuillez commenter!

atanamir
la source
2

Malheureusement, je n'ai pas assez de réputation pour commenter, alors je posterai une réponse. @Justin a soulevé le problème de l'exécution du formateur WebApiContrib.Formatting.Jsonp parallèlement au format JsonFormatter standard. Ce problème est résolu dans la dernière version (en fait publiée il y a quelque temps). En outre, il devrait fonctionner avec la dernière version de l'API Web.

panneaux de verre
la source
1

johperl, Thomas. La réponse donnée par Peter Moberg ci-dessus devrait être correcte pour la version RC car le JsonMediaTypeFormatter dont il hérite utilise déjà le sérialiseur NewtonSoft Json, et donc ce qu'il a devrait fonctionner sans aucun changement.

Cependant, pourquoi diable les gens utilisent-ils encore des paramètres, alors que vous pourriez simplement faire ce qui suit

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
la source
1

Au lieu d'héberger votre propre version du formateur JSONP, vous pouvez installer le package WebApiContrib.Formatting.Jsonp NuGet avec un déjà implémenté (choisissez la version qui fonctionne pour votre .NET Framework).

Ajoutez ce formateur dans Application_Start:

GlobalConfiguration.Configuration.Formatters.Insert(0, new JsonpMediaTypeFormatter(new JsonMediaTypeFormatter()));
M. Citrouille
la source
0

Pour ceux d'entre vous qui utilisent HttpSelfHostServer, cette section de code échouera sur HttpContext.Current, car elle n'existe pas sur le serveur auto-hôte.

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

Cependant, vous pouvez intercepter le «contexte» de l'hôte autonome via ce remplacement.

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

Le request.Method vous donnera "GET", "POST", etc. et le GetQueryNameValuePairs peut récupérer le paramètre de rappel?. Ainsi mon code révisé ressemble à:

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

J'espère que cela aide certains d'entre vous. De cette façon, vous n'avez pas nécessairement besoin d'un shim HttpContext.

C.

Coyote
la source
0

Si le contexte est Web Api, remerciant et faisant référence à 010227leola réponse de, vous devez considérer la WebContext.Currentvaleur qui va être null.

J'ai donc mis à jour son code pour ceci:

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
la source
0

Nous pouvons résoudre le problème CORS (Cross-origin resource sharing) de deux manières,

1) Utilisation de Jsonp 2) Activation du Cors

1) En utilisant Jsonp- pour utiliser le Jsonp, nous devons installer le package nuget WebApiContrib.Formatting.Jsonp et ajouter JsonpFormmater dans WebApiConfig.cs se référer aux captures d'écran,entrez la description de l'image ici

Code Jquery entrez la description de l'image ici

2) Activation du Cors -

pour activer les cors, nous devons ajouter le package nuget Microsoft.AspNet.WebApi.Cors et activer les cors dans WebApiConfig.cs, reportez-vous à la capture d'écran

entrez la description de l'image ici

Pour plus de références, vous pouvez consulter mon exemple de dépôt sur GitHub en utilisant le lien suivant. https://github.com/mahesh353/Ninject.WebAPi/tree/develop

Mendax
la source