JSONP com API da Web do ASP.NET


136

Estou trabalhando na criação de um novo conjunto de serviços no ASP.MVC MVC 4 usando a API da Web. Até agora, é ótimo. Eu criei o serviço e o fiz funcionar, e agora estou tentando consumi-lo usando o JQuery. Posso recuperar a string JSON usando o Fiddler, e parece estar ok, mas como o serviço existe em um site separado, tentando chamá-lo com erros de JQuery com o "Not Allowed". Portanto, esse é claramente um caso em que preciso usar o JSONP.

Sei que a API da Web é nova, mas espero que alguém possa me ajudar.

Como faço uma chamada para um método de API da Web usando JSONP?


1
Estava apenas analisando a nova estrutura da API da Web depois de assistir ao vídeo do ScottGu no Channel9 e ler o artigo de Scott Hanselman, e esse foi um dos meus primeiros pensamentos / perguntas sobre o assunto.
— precisa

Respostas:


132

Depois de fazer essa pergunta, finalmente encontrei o que precisava, por isso estou respondendo.

Encontrei este JsonpMediaTypeFormatter . Adicione-o Application_Startao seu global.asax fazendo o seguinte:

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

e você deve fazer uma chamada JQuery AJAX que se parece com isso:

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

Isso parece funcionar muito bem.


Parece não funcionar no meu caso, onde eu já tenho um formatador adicionado para serialização do Json.Net. Alguma ideia?
— 23712 Justin

4
Acredito FormatterContext é removido em MVC4 RC Versão forums.asp.net/post/5102318.aspx
— Diganta Kumar

13
O código agora faz parte do WebApiContrib no NuGet. Não há necessidade de puxá-lo manualmente.
— 31812 Jon Onstott

7
Sim, agora apenas: "Install-Package WebApiContrib.Formatting.Jsonp" O Doco está aqui: nuget.org/packages/WebApiContrib.Formatting.Jsonp
— nootn

4
Isto é o que eu tive que colocar usando de hoje de download NuGet:GlobalConfiguration.Configuration.AddJsonpFormatter(config.Formatters.JsonFormatter, "callback");
— joym8

52

Aqui está uma versão atualizada do JsonpMediaTypeFormatter para uso com o 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
Graças impressionantes, embora eu acredite que WriteToStreamAsync deve dar uma HttpContent não um HttpContentHeaders objeto agora na versão final, mas com essa mudança funcionou como um encanto
— Ben

21

Você pode usar um ActionFilterAttribute assim:

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

Em seguida, coloque-o em sua ação:

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

Funcionou perfeitamente com o VS2013 U5, MVC5.2 e WebApi 2
— Consulte Yarla

11

Certamente a resposta de Brian é a correta, no entanto, se você já estiver usando o formatador Json.Net, que fornece datas json bonitas e serialização mais rápida, não é possível adicionar apenas um segundo formatador para o jsonp, é preciso combinar os dois. É uma boa idéia usá-lo de qualquer maneira, como Scott Hanselman disse que o lançamento da API da Web do ASP.NET usará o serializador Json.Net por padrão.

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

Como podemos fazer isso para o ASP .NET Web API RC?
— jonperl

Também interessado na versão RC
— Thomas Stock



5

Atualizada

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

Obrigado, a outra versão não funciona na mais recente estrutura .net.
— precisa saber é o seguinte

2

Aqui está uma versão atualizada com várias melhorias, que funciona com a versão RTM das APIs da Web.

  • Seleciona a codificação correta, com base nos próprios Accept-Encodingcabeçalhos da solicitação . Os new StreamWriter()exemplos anteriores simplesmente usariam UTF-8. A chamada para base.WriteToStreamAsyncpode usar uma codificação diferente, resultando em saída corrompida.
  • Mapeia solicitações JSONP para o application/javascript Content-Typecabeçalho; o exemplo anterior produziria JSONP, mas com o application/jsoncabeçalho. Este trabalho é feito na Mappingclasse aninhada (cf. Melhor tipo de conteúdo para servir JSONP? )
  • Renuncia à sobrecarga de construção e liberação de ae StreamWriterobtém diretamente os bytes e os grava no fluxo de saída.
  • Em vez de aguardar uma tarefa, use o ContinueWithmecanismo da Biblioteca Paralela de Tarefas para encadear várias tarefas.

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
}

Estou ciente da "hackiness" do Func<string>parâmetro no construtor de classe interna, mas foi a maneira mais rápida de contornar o problema que resolve - como o C # só tem classes internas estáticas, ele não pode ver a CallbackQueryParameterpropriedade. Passar o Funcin vincula a propriedade no lambda, para que Mappingvocê possa acessá-lo posteriormente em TryMatchMediaType. Se você tem uma maneira mais elegante, comente!


2

Infelizmente, não tenho reputação suficiente para comentar, então postarei uma resposta. A @Justin levantou a questão da execução do formatador WebApiContrib.Formatting.Jsonp, juntamente com o JsonFormatter padrão. Esse problema foi resolvido na versão mais recente (lançada há algum tempo). Além disso, ele deve funcionar com a versão mais recente da API da Web.


1

johperl, Thomas. A resposta dada por Peter Moberg acima deve estar correta para a versão RC, como o JsonMediaTypeFormatter que ele herdou já usa o serializador NewtonSoft Json e, portanto, o que ele tem deve funcionar sem nenhuma alteração.

No entanto, por que diabos as pessoas ainda usam parâmetros, quando você pode fazer o seguinte

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

Em vez de hospedar sua própria versão do formatador JSONP, você pode instalar o pacote WebApiContrib.Formatting.Jsonp NuGet com o já implementado (escolha a versão que funciona com o .NET Framework).

Adicione este formatador em Application_Start:

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

0

Para aqueles que estão usando o HttpSelfHostServer, esta seção do código falhará no HttpContext.Current, pois não existe no servidor auto-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);
 }

No entanto, você pode interceptar o "contexto" do host próprio por meio dessa substituição.

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

O request.Method fornece "GET", "POST" etc. e o GetQueryNameValuePairs pode recuperar o parâmetro? Callback. Assim, meu código revisado se parece com:

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 isso ajude alguns de vocês. Dessa forma, você não precisa necessariamente de um calço HttpContext.

C.



0

Se o contexto é Web Api, agradecendo e se referindo à 010227leoresposta de, você deve considerar o WebContext.Currentvalor que será null.

Então eu atualizei o código dele para isso:

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

Podemos resolver o problema do CORS (compartilhamento de recursos de origem cruzada) usando duas maneiras,

1) Usando Jsonp 2) Habilitando o Cors

1) Usando o Jsonp- para usar o Jsonp, precisamos instalar o pacote de nuget WebApiContrib.Formatting.Jsonp e adicionar JsonpFormmater no WebApiConfig.cs, consulte capturas de tela,insira a descrição da imagem aqui

Código Jquery insira a descrição da imagem aqui

2) Habilitando o Cors -

para habilitar os cors, precisamos adicionar o pacote de nuget Microsoft.AspNet.WebApi.Cors e precisamos habilitar os cors no WebApiConfig.cs

insira a descrição da imagem aqui

Para obter mais referências, você pode consultar meu repositório de amostra no GitHub usando o link a seguir. https://github.com/mahesh353/Ninject.WebAPi/tree/develop

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.