JSONP con API web ASP.NET

136

Estoy trabajando en la creación de un nuevo conjunto de servicios en ASP.MVC MVC 4 utilizando la API web. Hasta ahora, es genial. Creé el servicio y lo puse a trabajar, y ahora estoy tratando de consumirlo usando JQuery. Puedo recuperar la cadena JSON usando Fiddler, y parece estar bien, pero debido a que el servicio existe en un sitio separado, intento llamarlo con errores de JQuery con "No permitido". Entonces, este es claramente un caso en el que necesito usar JSONP.

Sé que la API web es nueva, pero espero que alguien pueda ayudarme.

¿Cómo hago una llamada a un método de API web usando JSONP?

Brian McCord
fuente
1
Estaba mirando la nueva estructura de la API web después de ver el video de ScottGu en Channel9, y leer el artículo de Scott Hanselman, y este fue uno de mis primeros pensamientos / preguntas sobre esto.
Rastreador1

Respuestas:

132

Después de hacer esta pregunta, finalmente encontré lo que necesitaba, así que estoy respondiendo.

Me encontré con este JsonpMediaTypeFormatter . Agregarlo a la Application_Startde su global.asax al hacer esto:

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

y es bueno ir con una llamada JQuery AJAX que se ve así:

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

Parece funcionar muy bien.

Brian McCord
fuente
No parece funcionar en mi caso, donde ya tengo un formateador agregado para la serialización de Json.Net. ¿Algunas ideas?
Justin
44
Creo que FormatterContext se elimina en MVC4 RC Version foros.asp.net/post/5102318.aspx
Diganta Kumar
13
El código ahora es parte de WebApiContrib en NuGet. No es necesario jalarlo manualmente.
Jon Onstott
77
Sí, ahora solo: "Install-Package WebApiContrib.Formatting.Jsonp" Doco está aquí: nuget.org/packages/WebApiContrib.Formatting.Jsonp
nootn
44
Esto es lo que tuve que poner usando la descarga nuget de hoy:GlobalConfiguration.Configuration.AddJsonpFormatter(config.Formatters.JsonFormatter, "callback");
joym8
52

Aquí hay una versión actualizada de JsonpMediaTypeFormatter para usar 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);
    }
}
Peter Moberg
fuente
8
Impresionantes gracias, aunque creo que WriteToStreamAsync debería tomar un HttpContent, no un objeto HttpContentHeaders ahora en la versión final, pero con ese cambio funcionó de maravilla
Ben
21

Puedes usar un ActionFilterAttribute como este:

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

Luego ponlo en tu acción:

[JsonCallback]
public IEnumerable<User> User()
{
    return _user;
}
010227leo
fuente
Funcionó perfectamente con VS2013 U5, MVC5.2 y WebApi 2
Consulte a Yarla el
11

Ciertamente, la respuesta de Brian es la correcta, sin embargo, si ya está utilizando el formateador Json.Net, que le da fechas json bonitas y una serialización más rápida, entonces no puede simplemente agregar un segundo formateador para jsonp, debe combinar los dos. Es una buena idea usarlo de todos modos, ya que Scott Hanselman ha dicho que el lanzamiento de la API web ASP.NET va a usar el serializador Json.Net de forma predeterminada.

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
fuente
¿Cómo podemos hacer esto para ASP .NET Web API RC?
jonperl
también interesado en la versión RC
Thomas Stock
6

JSONP solo funciona con la solicitud Http GET. Hay un soporte CORS en asp.net web api que funciona bien con todos los verbos http.

Este artículo puede ser útil para usted.

usuario1186065
fuente
1
Ahora hay compatibilidad con CORS en la API web. Este artículo es bastante útil - asp.net/web-api/overview/security/…
Ilia Barahovski
5

Actualizado

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
fuente
Gracias, la otra versión no funciona en el último framework .net.
djbielejeski
2

Aquí hay una versión actualizada con varias mejoras, que funciona con la versión RTM de las API web.

  • Selecciona la codificación correcta, en función de los Accept-Encodingencabezados de la solicitud . En new StreamWriter()los ejemplos anteriores simplemente usaría UTF-8. La llamada a base.WriteToStreamAsyncpuede usar una codificación diferente, lo que da como resultado una salida corrupta.
  • Asigna solicitudes JSONP al application/javascript Content-Typeencabezado; el ejemplo anterior generaría JSONP, pero con el application/jsonencabezado. Este trabajo se realiza en la Mappingclase anidada (cf. ¿El mejor tipo de contenido para servir JSONP? )
  • Renuncia a la construcción y al vaciado general de ay StreamWriterobtiene directamente los bytes y los escribe en la secuencia de salida.
  • En lugar de esperar una tarea, use el ContinueWithmecanismo de la Biblioteca Paralela de Tareas para encadenar varias tareas.

Código:

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
}

Soy consciente de la "piratería" del Func<string>parámetro en el constructor de la clase interna, pero fue la forma más rápida de solucionar el problema que resuelve, ya que C # solo tiene clases internas estáticas, no puede ver la CallbackQueryParameterpropiedad. Al pasar Funcin, se vincula la propiedad en lambda, por Mappinglo que podrá acceder a ella más adelante TryMatchMediaType. Si tienes una forma más elegante, ¡por favor comenta!

atanamir
fuente
2

Desafortunadamente, no tengo suficiente reputación para comentar, así que publicaré una respuesta. @Justin planteó el problema de ejecutar el formateador WebApiContrib.Formatting.Jsonp junto con el JsonFormatter estándar. Ese problema se resuelve en la última versión (que se lanzó hace algún tiempo). Además, debería funcionar con la última versión de API web.

Panesofglass
fuente
1

johperl, Thomas. La respuesta dada por Peter Moberg arriba debe ser correcta para la versión RC ya que JsonMediaTypeFormatter que hereda usa el serializador NewtonSoft Json, y lo que tiene debería funcionar sin ningún cambio.

Sin embargo, ¿por qué demonios la gente todavía usa parámetros? ¿Cuándo podría hacer lo siguiente?

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);
        }
hilo de rosca
fuente
1

En lugar de alojar su propia versión de formateador JSONP, puede instalar el paquete WebApiContrib.Formatting.Jsonp NuGet con uno ya implementado (elija la versión que funcione para su .NET Framework).

Agregue este formateador en Application_Start:

GlobalConfiguration.Configuration.Formatters.Insert(0, new JsonpMediaTypeFormatter(new JsonMediaTypeFormatter()));
Señor calabaza
fuente
0

Para aquellos de ustedes que están utilizando el HttpSelfHostServer, esta sección del código fallará en HttpContext.Current, ya que no existe en el servidor de 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);
 }

Sin embargo, puede interceptar el "contexto" del propio host a través de esta anulación.

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

El método request.Metho le dará "GET", "POST", etc. y el GetQueryNameValuePairs puede recuperar el parámetro? Callback. Por lo tanto, mi código revisado se ve así:

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

Espero que esto ayude a alguno de ustedes. De esta manera, no necesariamente necesita una cuña HttpContext.

C.

Coyote
fuente
0

Si el contexto es Web Api, agradeciendo y refiriéndose a 010227leola respuesta, debe considerar el WebContext.Currentvalor que va a ser null.

Así que actualicé su código a esto:

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

Podemos resolver el problema de CORS (intercambio de recursos de origen cruzado) de dos maneras,

1) Usando Jsonp 2) Habilitando los Cors

1) Usando Jsonp- para usar Jsonp, necesitamos instalar el paquete Nuget WebApiContrib.Formatting.Jsonp y necesitamos agregar JsonpFormmater en WebApiConfig.cs referir capturas de pantalla,ingrese la descripción de la imagen aquí

Código jquery ingrese la descripción de la imagen aquí

2) Habilitar los Cors -

para habilitar los cors necesitamos agregar el paquete nuget Microsoft.AspNet.WebApi.Cors y necesitamos habilitar los cors en WebApiConfig.cs consulte la captura de pantalla

ingrese la descripción de la imagen aquí

Para obtener más referencia, puede consultar mi repositorio de muestra en GitHub usando el siguiente enlace. https://github.com/mahesh353/Ninject.WebAPi/tree/develop

Mendax
fuente