Criar string de consulta para obter System.Net.HttpClient

184

Se eu desejar enviar uma solicitação de obtenção http usando System.Net.HttpClient, parece não haver API para adicionar parâmetros, isso está correto?

Existe alguma API simples disponível para criar a string de consulta que não envolva a criação de uma coleção de valores de nomes e URLs que codifiquem esses itens e, finalmente, concatená-los? Eu esperava usar algo como a API do RestSharp (por exemplo, AddParameter (..))

NeoDarque
fonte
@ Michael Perrenoud você pode querer reconsiderar usando a resposta aceita com personagens que codificação necessidade, ver a minha explicação abaixo
ilegal-imigrante

Respostas:

309

Se eu desejar enviar uma solicitação de obtenção http usando System.Net.HttpClient, parece não haver API para adicionar parâmetros, isso está correto?

Sim.

Existe alguma API simples disponível para criar a string de consulta que não envolva a criação de uma coleção de valores de nomes e URLs que codifiquem esses itens e, finalmente, concatená-los?

Certo:

var query = HttpUtility.ParseQueryString(string.Empty);
query["foo"] = "bar<>&-baz";
query["bar"] = "bazinga";
string queryString = query.ToString();

lhe dará o resultado esperado:

foo=bar%3c%3e%26-baz&bar=bazinga

Você também pode achar UriBuilderútil a classe:

var builder = new UriBuilder("http://example.com");
builder.Port = -1;
var query = HttpUtility.ParseQueryString(builder.Query);
query["foo"] = "bar<>&-baz";
query["bar"] = "bazinga";
builder.Query = query.ToString();
string url = builder.ToString();

lhe dará o resultado esperado:

http://example.com/?foo=bar%3c%3e%26-baz&bar=bazinga

que você poderia alimentar com mais segurança o seu HttpClient.GetAsyncmétodo.

Darin Dimitrov
fonte
9
Esse é o melhor absoluto em termos de manipulação de URL no .NET. Não há necessidade de codificação manual de URLs e concatenações de caracteres, construtores de caracteres ou qualquer outra coisa. A classe UriBuilder manipula URLs com fragmentos ( #) para você usar a propriedade Fragment. Eu já vi tantas pessoas cometendo o erro de manipular manualmente os URLs em vez de usar as ferramentas internas.
Darin Dimitrov
6
NameValueCollection.ToString()normalmente não cria seqüências de caracteres de consulta, e não há documentação declarando que fazer um ToStringno resultado de ParseQueryStringresultará em uma nova sequência de caracteres de consulta, portanto, pode ser interrompida a qualquer momento, pois não há garantia nessa funcionalidade.
Matthew
11
HttpUtility está no System.Web, que não está disponível no tempo de execução portátil. Parece estranho que essa funcionalidade geralmente não esteja mais disponível nas bibliotecas de classes.
31913 Chris Eldredge
82
Esta solução é desprezível. .Net deve ter o construtor de querystring adequado.
precisa saber é o seguinte
8
O fato de a melhor solução estar oculta na classe interna para a qual você só pode obter chamando um método utilitário que passa na cadeia vazia não pode ser exatamente chamado de solução elegante.
Kugel #
79

Para aqueles que não desejam incluir System.Webem projetos que ainda não o utilizam, você pode usar a FormUrlEncodedContentpartir deSystem.Net.Http e fazer algo como o seguinte:

keyvaluepair version

string query;
using(var content = new FormUrlEncodedContent(new KeyValuePair<string, string>[]{
    new KeyValuePair<string, string>("ham", "Glazed?"),
    new KeyValuePair<string, string>("x-men", "Wolverine + Logan"),
    new KeyValuePair<string, string>("Time", DateTime.UtcNow.ToString()),
})) {
    query = content.ReadAsStringAsync().Result;
}

versão do dicionário

string query;
using(var content = new FormUrlEncodedContent(new Dictionary<string, string>()
{
    { "ham", "Glaced?"},
    { "x-men", "Wolverine + Logan"},
    { "Time", DateTime.UtcNow.ToString() },
})) {
    query = content.ReadAsStringAsync().Result;
}
Rostov
fonte
Por que você usa uma instrução using?
Ian Warburton
É provável que libere recursos, mas isso é exagerado. Não faça isso.
Kody
5
Isso pode ser mais conciso usando o Dictionary <string, string> em vez da matriz KVP. Em seguida, use a sintaxe do inicializador de: {"ham", "Glazed?" }
Sean B
@SeanB Essa é uma boa ideia, especialmente ao usar algo para adicionar uma lista dinâmica / desconhecida de parâmetros. Para este exemplo, por ser uma lista "fixa", não achei que a sobrecarga de um dicionário valesse a pena.
Rostov
6
@Kody Por que você diz para não usar dispose? Eu sempre descarto a menos que tenha um bom motivo para não fazê-lo, como reutilizar HttpClient.
Dan Friedman
41

TL; DR: não use a versão aceita, pois está completamente corrompida em relação ao tratamento de caracteres unicode e nunca use a API interna

Na verdade, eu encontrei um problema estranho de codificação dupla com a solução aceita:

Portanto, se você estiver lidando com caracteres que precisam ser codificados, a solução aceita levará à codificação dupla:

  • os parâmetros de consulta são codificados automaticamente usando o NameValueCollectionindexador ( e isso usa UrlEncodeUnicode, não o esperado regularmente UrlEncode(!) )
  • Então, quando você o chama uriBuilder.Uri, cria um novo Uriconstrutor usando, que codifica mais uma vez (codificação de URL normal)
  • Isso não pode ser evitado fazendouriBuilder.ToString() (mesmo que isso retorne correto, Uriqual IMO é pelo menos inconsistente, talvez um bug, mas essa é outra questão) e, em seguida, usando o HttpClientmétodo de aceitação de string - o cliente ainda cria Urisua string passada da seguinte maneira:new Uri(uri, UriKind.RelativeOrAbsolute)

Repro pequeno, mas completo:

var builder = new UriBuilder
{
    Scheme = Uri.UriSchemeHttps,
    Port = -1,
    Host = "127.0.0.1",
    Path = "app"
};

NameValueCollection query = HttpUtility.ParseQueryString(builder.Query);

query["cyrillic"] = "кирилиця";

builder.Query = query.ToString();
Console.WriteLine(builder.Query); //query with cyrillic stuff UrlEncodedUnicode, and that's not what you want

var uri = builder.Uri; // creates new Uri using constructor which does encode and messes cyrillic parameter even more
Console.WriteLine(uri);

// this is still wrong:
var stringUri = builder.ToString(); // returns more 'correct' (still `UrlEncodedUnicode`, but at least once, not twice)
new HttpClient().GetStringAsync(stringUri); // this creates Uri object out of 'stringUri' so we still end up sending double encoded cyrillic text to server. Ouch!

Resultado:

?cyrillic=%u043a%u0438%u0440%u0438%u043b%u0438%u0446%u044f

https://127.0.0.1/app?cyrillic=%25u043a%25u0438%25u0440%25u0438%25u043b%25u0438%25u0446%25u044f

Como você pode ver, não importa se você faz uribuilder.ToString()+ httpClient.GetStringAsync(string)ou uriBuilder.Uri+httpClient.GetStringAsync(Uri) você acaba enviando parâmetro codificado duplo

Exemplo fixo pode ser:

var uri = new Uri(builder.ToString(), dontEscape: true);
new HttpClient().GetStringAsync(uri);

Mas isso usa construtor obsoleto Uri

PS no meu .NET mais recente no Windows Server, Uri construtor com comentário bool doc diz "obsoleto, o dontEscape é sempre falso", mas realmente funciona conforme o esperado (ignora a fuga)

Então parece outro bug ...

E mesmo isso está completamente errado - ele envia UrlEncodedUnicode para o servidor, não apenas UrlEncoded o que o servidor espera

Atualização: mais uma coisa é que NameValueCollection realmente faz UrlEncodeUnicode, que não deve mais ser usado e é incompatível com url.encode / decode regular (consulte NameValueCollection to URL Query? ).

Portanto, o ponto principal é: nunca use esse hack,NameValueCollection query = HttpUtility.ParseQueryString(builder.Query); pois isso interferirá nos parâmetros de consulta unicode. Basta criar a consulta manualmente e atribuí-la à UriBuilder.Querycodificação necessária e, em seguida, obter o Uri UriBuilder.Uri.

Exemplo excelente de se machucar usando código que não deve ser usado assim

imigrante ilegal
fonte
16
Você poderia adicionar uma função de utilitário completa a esta resposta que funciona?
Mafu
8
Em segundo lugar, mafu: li a resposta, mas não tenho uma conclusão. Existe uma resposta definitiva para isso?
Richard Griffiths
3
Eu também gostaria de ver a resposta definitiva para este problema
Pones
A resposta definitiva para esse problema é usar var namedValues = HttpUtility.ParseQueryString(builder.Query), mas, em vez de usar o NameValueCollection retornado, converta-o imediatamente em um Dicionário da seguinte forma: var dic = values.ToDictionary(x => x, x => values[x]); Adicione novos valores ao dicionário, passe-o para o construtor FormUrlEncodedContente ligue ReadAsStringAsync().Result-o. Isso fornece uma string de consulta codificada corretamente, que você pode atribuir de volta ao UriBuilder.
Triynko
Você realmente pode apenas usar NamedValueCollection.ToString em vez de tudo isso, mas somente se você alterar um app.config / web.config configuração que impede que ASP.NET de usar o '% uXXXX' encoding: <add key="aspnet:DontUsePercentUUrlEncoding" value="true" />. Como eu não dependeria desse comportamento, é melhor usar a classe FormUrlEncodedContent, como demonstrado por uma resposta anterior: stackoverflow.com/a/26744471/88409
Triynko
41

Em um projeto ASP.NET Core, você pode usar a classe QueryHelpers.

// using Microsoft.AspNetCore.WebUtilities;
var query = new Dictionary<string, string>
{
    ["foo"] = "bar",
    ["foo2"] = "bar2",
    // ...
};

var response = await client.GetAsync(QueryHelpers.AddQueryString("/api/", query));
Magu
fonte
2
É irritante que, embora com esse processo, você ainda não possa enviar vários valores para a mesma chave. Se você deseja enviar "bar" e "bar2" como parte de apenas foo, não é possível.
m0g
2
Esta é uma ótima resposta para aplicativos modernos, funciona no meu cenário, simples e limpo. No entanto, não preciso de nenhum mecanismo de escape - não testado.
Patrick Stalph
Este alvos de pacotes NuGet .NET 2.0 padrão que significa que você pode usá-lo no framework .NET pleno 4.6.1+
eddiewould
24

Você pode conferir o Flurl [divulgação: sou o autor], um construtor de URLs fluente com lib complementar opcional que o estende a um cliente REST completo.

var result = await "https://api.com"
    // basic URL building:
    .AppendPathSegment("endpoint")
    .SetQueryParams(new {
        api_key = ConfigurationManager.AppSettings["SomeApiKey"],
        max_results = 20,
        q = "Don't worry, I'll get encoded!"
    })
    .SetQueryParams(myDictionary)
    .SetQueryParam("q", "overwrite q!")

    // extensions provided by Flurl.Http:
    .WithOAuthBearerToken("token")
    .GetJsonAsync<TResult>();

Confira os documentos para mais detalhes. O pacote completo está disponível no NuGet:

PM> Install-Package Flurl.Http

ou apenas o construtor de URL independente:

PM> Install-Package Flurl

Todd Menier
fonte
2
Por que não estender Uriou começar com sua própria classe em vez de string?
MPEN
2
Tecnicamente, comecei com minha própria Urlclasse. O acima é equivalente a new Url("https://api.com").AppendPathSegment...Pessoalmente, prefiro as extensões de string devido a menos pressionamentos de tecla e padronizadas nos documentos, mas você pode fazê-lo de qualquer maneira.
Todd Menier
Fora do tópico, mas muito legal, estou usando depois de ver isso. Obrigado por usar o IHttpClientFactory também.
Ed S.
4

Na mesma linha da postagem de Rostov, se você não deseja incluir uma referência System.Webem seu projeto, pode usar a FormDataCollectionpartir deSystem.Net.Http.Formatting e fazer algo como o seguinte:

Usando System.Net.Http.Formatting.FormDataCollection

var parameters = new Dictionary<string, string>()
{
    { "ham", "Glaced?" },
    { "x-men", "Wolverine + Logan" },
    { "Time", DateTime.UtcNow.ToString() },
}; 
var query = new FormDataCollection(parameters).ReadAsNameValueCollection().ToString();
cwills
fonte
3

Darin ofereceu uma solução interessante e inteligente, e aqui está algo que pode ser outra opção:

public class ParameterCollection
{
    private Dictionary<string, string> _parms = new Dictionary<string, string>();

    public void Add(string key, string val)
    {
        if (_parms.ContainsKey(key))
        {
            throw new InvalidOperationException(string.Format("The key {0} already exists.", key));
        }
        _parms.Add(key, val);
    }

    public override string ToString()
    {
        var server = HttpContext.Current.Server;
        var sb = new StringBuilder();
        foreach (var kvp in _parms)
        {
            if (sb.Length > 0) { sb.Append("&"); }
            sb.AppendFormat("{0}={1}",
                server.UrlEncode(kvp.Key),
                server.UrlEncode(kvp.Value));
        }
        return sb.ToString();
    }
}

e, portanto, ao usá-lo, você pode fazer o seguinte:

var parms = new ParameterCollection();
parms.Add("key", "value");

var url = ...
url += "?" + parms;
Mike Perrenoud
fonte
5
Você deseja codificar kvp.Keye kvp.Valueseparadamente dentro do loop for, não na string de consulta completa (portanto, não codificando os caracteres &e =).
Matthew
Obrigado Mike. As outras soluções propostas (envolvendo NameValueCollection) não funcionaram para mim porque estou em um projeto PCL, portanto, essa foi uma alternativa perfeita. Para outros que estão trabalhando no lado do cliente, o server.UrlEncodepode ser substituído porWebUtility.UrlEncode
BCA /
2

Ou simplesmente usando minha extensão Uri

Código

public static Uri AttachParameters(this Uri uri, NameValueCollection parameters)
{
    var stringBuilder = new StringBuilder();
    string str = "?";
    for (int index = 0; index < parameters.Count; ++index)
    {
        stringBuilder.Append(str + parameters.AllKeys[index] + "=" + parameters[index]);
        str = "&";
    }
    return new Uri(uri + stringBuilder.ToString());
}

Uso

Uri uri = new Uri("http://www.example.com/index.php").AttachParameters(new NameValueCollection
                                                                           {
                                                                               {"Bill", "Gates"},
                                                                               {"Steve", "Jobs"}
                                                                           });

Resultado

http://www.example.com/index.php?Bill=Gates&Steve=Jobs

Roman Ratskey
fonte
27
Você não esqueceu a codificação de URL?
Kugel
1
este é um ótimo exemplo de uso de extensões para criar ajudantes claros e úteis. Se você combinar isso com a resposta aceita, estará no caminho de criar um RestClient sólido
emran
2

A biblioteca de modelos de URI RFC 6570 que estou desenvolvendo é capaz de executar esta operação. Toda a codificação é manipulada para você de acordo com essa RFC. No momento da redação deste artigo, uma versão beta está disponível e a única razão pela qual não é considerada uma versão 1.0 estável é que a documentação não atende totalmente às minhas expectativas (consulte as edições 17 , 18 , 32 e 43 ).

Você pode criar uma string de consulta sozinha:

UriTemplate template = new UriTemplate("{?params*}");
var parameters = new Dictionary<string, string>
  {
    { "param1", "value1" },
    { "param2", "value2" },
  };
Uri relativeUri = template.BindByName(parameters);

Ou você pode criar um URI completo:

UriTemplate template = new UriTemplate("path/to/item{?params*}");
var parameters = new Dictionary<string, string>
  {
    { "param1", "value1" },
    { "param2", "value2" },
  };
Uri baseAddress = new Uri("http://www.example.com");
Uri relativeUri = template.BindByName(baseAddress, parameters);
Sam Harwell
fonte
1

Como tenho que reutilizar esse período de tempo, criei essa classe que simplesmente ajuda a abstrair como a string de consulta é composta.

public class UriBuilderExt
{
    private NameValueCollection collection;
    private UriBuilder builder;

    public UriBuilderExt(string uri)
    {
        builder = new UriBuilder(uri);
        collection = System.Web.HttpUtility.ParseQueryString(string.Empty);
    }

    public void AddParameter(string key, string value) {
        collection.Add(key, value);
    }

    public Uri Uri{
        get
        {
            builder.Query = collection.ToString();
            return builder.Uri;
        }
    }

}

O uso será simplificado para algo como isto:

var builder = new UriBuilderExt("http://example.com/");
builder.AddParameter("foo", "bar<>&-baz");
builder.AddParameter("bar", "second");
var uri = builder.Uri;

que retornará a URL : http://example.com/?foo=bar%3c%3e%26-baz&bar=second

Jaider
fonte
1

Para evitar o problema de codificação dupla descrito na resposta do taras.roshko e manter a possibilidade de trabalhar facilmente com os parâmetros de consulta, você pode usar em uriBuilder.Uri.ParseQueryString()vez de HttpUtility.ParseQueryString().

Valeriy Lyuchyn
fonte
1

Boa parte da resposta aceita foi modificada para usar UriBuilder.Uri.ParseQueryString () em vez de HttpUtility.ParseQueryString ():

var builder = new UriBuilder("http://example.com");
var query = builder.Uri.ParseQueryString();
query["foo"] = "bar<>&-baz";
query["bar"] = "bazinga";
builder.Query = query.ToString();
string url = builder.ToString();
Jpsy
fonte
FYI: Isso requer uma referência ao System.Net.Http, pois o ParseQueryString()método de extensão não está dentro System.
Sunny Patel
0

Graças a "Darin Dimitrov", este é o método de extensão.

 public static partial class Ext
{
    public static Uri GetUriWithparameters(this Uri uri,Dictionary<string,string> queryParams = null,int port = -1)
    {
        var builder = new UriBuilder(uri);
        builder.Port = port;
        if(null != queryParams && 0 < queryParams.Count)
        {
            var query = HttpUtility.ParseQueryString(builder.Query);
            foreach(var item in queryParams)
            {
                query[item.Key] = item.Value;
            }
            builder.Query = query.ToString();
        }
        return builder.Uri;
    }

    public static string GetUriWithparameters(string uri,Dictionary<string,string> queryParams = null,int port = -1)
    {
        var builder = new UriBuilder(uri);
        builder.Port = port;
        if(null != queryParams && 0 < queryParams.Count)
        {
            var query = HttpUtility.ParseQueryString(builder.Query);
            foreach(var item in queryParams)
            {
                query[item.Key] = item.Value;
            }
            builder.Query = query.ToString();
        }
        return builder.Uri.ToString();
    }
}
Waleed AK
fonte
-1

Não consegui encontrar uma solução melhor do que criar um método de extensão para converter um Dicionário em QueryStringFormat. A solução proposta por Waleed AK também é boa.

Siga minha solução:

Crie o método de extensão:

public static class DictionaryExt
{
    public static string ToQueryString<TKey, TValue>(this Dictionary<TKey, TValue> dictionary)
    {
        return ToQueryString<TKey, TValue>(dictionary, "?");
    }

    public static string ToQueryString<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, string startupDelimiter)
    {
        string result = string.Empty;
        foreach (var item in dictionary)
        {
            if (string.IsNullOrEmpty(result))
                result += startupDelimiter; // "?";
            else
                result += "&";

            result += string.Format("{0}={1}", item.Key, item.Value);
        }
        return result;
    }
}

E eles:

var param = new Dictionary<string, string>
          {
            { "param1", "value1" },
            { "param2", "value2" },
          };
param.ToQueryString(); //By default will add (?) question mark at begining
//"?param1=value1&param2=value2"
param.ToQueryString("&"); //Will add (&)
//"&param1=value1&param2=value2"
param.ToQueryString(""); //Won't add anything
//"param1=value1&param2=value2"
Diego Mendes
fonte
1
Esta solução está faltando codificação de URL adequada de parâmetros e não vai funcionar com os valores que contêm caracteres inválidos ''
Xavier Poinas
Sinta-se livre para atualizar a resposta e adicionar a linha de codificação ausente, é apenas uma linha de código!
Diego Mendes