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?

Brian McCord
fonte
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.

Brian McCord
fonte
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);
    }
}
Peter Moberg
fonte
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;
}
010227leo
fonte
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);
        }
    }
Justin
fonte
Como podemos fazer isso para o ASP .NET Web API RC?
jonperl
Também interessado na versão RC
Thomas Stock
6

JSONP funciona apenas com solicitação GET HTTP. Há um suporte ao CORS na API da web asp.net que funciona bem com todos os verbos http.

Este artigo pode ser útil para você.

user1186065
fonte
1
Agora há suporte ao CORS na Web API. Este artigo é bastante útil - asp.net/web-api/overview/security/…
Ilia Barahovski 4/14
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);
        }
    }
ITXGEN
fonte
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!

atanamir
fonte
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.

panesofglass
fonte
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);
        }
stevethethread
fonte
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()));
Mr. Pumpkin
fonte
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.

Coiote
fonte
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);
    }
}
Rikki
fonte
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

Mendax
fonte