Crea una stringa di query per System.Net.HttpClient get


184

Se desidero inviare una richiesta di get http utilizzando System.Net.HttpClient non sembra esserci api per aggiungere parametri, è corretto?

C'è qualche semplice API disponibile per creare la stringa di query che non comporta la creazione di una raccolta di valori di nomi e l'URL che codifica quelli e infine li concatena? Speravo di usare qualcosa come l'API di RestSharp (cioè AddParameter (..))


@Michael Perrenoud potresti voler riconsiderare usando la risposta accettata con i personaggi che hanno bisogno di essere codificati, vedi la mia spiegazione di seguito
immigrato clandestino

Risposte:


309

Se desidero inviare una richiesta di get http utilizzando System.Net.HttpClient non sembra esserci api per aggiungere parametri, è corretto?

Sì.

C'è qualche semplice API disponibile per creare la stringa di query che non comporta la creazione di una raccolta di valori di nomi e l'URL che codifica quelli e infine li concatena?

Sicuro:

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

ti darà il risultato atteso:

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

Potresti anche trovare UriBuilderutile la 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();

ti darà il risultato atteso:

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

che potresti più che tranquillamente alimentare il tuo HttpClient.GetAsyncmetodo.


9
Questo è il migliore in assoluto in termini di gestione degli URL in .NET. Non c'è mai bisogno di urlare manualmente la codifica e fare concatenazioni di stringhe o costruttori di stringhe o altro. La classe UriBuilder gestirà anche gli URL con fragments ( #) usando la proprietà Fragment. Ho visto così tante persone fare l'errore di gestire manualmente gli URL invece di usare gli strumenti integrati.
Darin Dimitrov,

6
NameValueCollection.ToString()normalmente non crea stringhe di query e non esiste documentazione che indichi che eseguire un ToStringrisultato sul risultato ParseQueryStringsi tradurrà in una nuova stringa di query, quindi potrebbe interrompersi in qualsiasi momento poiché non esiste alcuna garanzia in tale funzionalità.
Matteo,

11
HttpUtility è in System.Web che non è disponibile in runtime portatile. Sembra strano che questa funzionalità non sia più generalmente disponibile nelle librerie di classi.
Chris Eldredge,

82
Questa soluzione è spregevole. .Net dovrebbe avere un generatore di querystring adeguato.
Kugel,

8
Il fatto che la migliore soluzione sia nascosta nella classe interna alla quale è possibile ottenere solo chiamando un metodo di utilità che passa in una stringa vuota non può essere esattamente definita una soluzione elegante.
Kugel,

79

Per coloro che non vogliono includere System.Webnei progetti che non hanno già lo utilizzano, è possibile utilizzare FormUrlEncodedContentda System.Net.Httpe fare qualcosa di simile alla seguente:

versione keyvaluepair

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

versione del dizionario

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

Perché usi un'istruzione using?
Ian Warburton,

Probabile liberare risorse, ma questo è esagerato. Non farlo
Kody,

5
Questo può essere più conciso usando Dizionario <stringa, stringa> invece dell'array KVP. Quindi usando la sintassi dell'inizializzatore di: {"ham", "Glazed?" }
Sean B,

@SeanB È una buona idea, soprattutto quando si utilizza qualcosa per aggiungere un elenco dinamico / sconosciuto di parametri. Per questo esempio, dato che è un elenco "fisso", non pensavo che ne valesse la pena il sovraccarico di un dizionario.
Rostov,

6
@Kody Perché dici di non usare dispose? Dispongo sempre a meno che non abbia una buona ragione per non farlo, come riutilizzare HttpClient.
Dan Friedman,

41

TL; DR: non utilizzare la versione accettata poiché è completamente rotto in relazione alla gestione dei caratteri unicode e non utilizzare mai l'API interna

In realtà ho riscontrato uno strano problema di doppia codifica con la soluzione accettata:

Quindi, se hai a che fare con caratteri che devono essere codificati, la soluzione accettata porta alla doppia codifica:

  • i parametri della query vengono codificati automaticamente utilizzando l' NameValueCollectionindicizzatore ( e questo utilizza UrlEncodeUnicode, non previsto normalmente UrlEncode(!) )
  • Quindi, quando lo chiami uriBuilder.Uri, crea un nuovo Uriusando il costruttore che codifica ancora una volta (codifica URL normale)
  • Questo non può essere evitato facendouriBuilder.ToString() (anche se questo restituisce correttamente Uriquale IMO è almeno incoerenza, forse un bug, ma questa è un'altra domanda) e quindi usando il HttpClientmetodo che accetta la stringa - il client crea ancora Uridalla tua stringa passata in questo modo:new Uri(uri, UriKind.RelativeOrAbsolute)

Riproduzione piccola ma completa:

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!

Produzione:

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

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

Come puoi vedere, non importa se fai uribuilder.ToString()+ httpClient.GetStringAsync(string)o uriBuilder.Uri+ httpClient.GetStringAsync(Uri)finisci per inviare un doppio parametro codificato

L'esempio fisso potrebbe essere:

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

Ma questo utilizza un costruttore obsoleto Uri

PS sul mio ultimo .NET su Windows Server, il Uricostruttore con il commento bool doc dice "obsoleto, dontEscape è sempre falso", ma in realtà funziona come previsto (salta la fuga)

Quindi sembra un altro bug ...

E anche questo è assolutamente sbagliato: invia UrlEncodedUnicode al server, non solo UrlEncoded ciò che il server si aspetta

Aggiornamento: un'altra cosa è, NameValueCollection in realtà fa UrlEncodeUnicode, che non dovrebbe più essere usato ed è incompatibile con il normale url.encode / decode (vedi NameValueCollection alla query URL? ).

Quindi la linea di fondo è: non usare mai questo hack conNameValueCollection query = HttpUtility.ParseQueryString(builder.Query); in quanto rovinerà i parametri della query unicode. Basta compilare manualmente la query e assegnarla alla UriBuilder.Queryquale farà la codifica necessaria e quindi ottenere Uri utilizzando UriBuilder.Uri.

Primo esempio di farsi del male usando il codice che non dovrebbe essere usato in questo modo


16
Potresti aggiungere una funzione di utilità completa a questa risposta che funziona?
mafu,

8
Io secondo mafu su questo: ho letto la risposta ma non ho una conclusione. C'è una risposta definitiva a questo?
Richard Griffiths,

3
Vorrei anche vedere la risposta definitiva a questo problema
Pones,

La risposta definitiva a questo problema è usare var namedValues = HttpUtility.ParseQueryString(builder.Query), ma invece di usare il NameValueCollection restituito, convertilo immediatamente in un dizionario in questo modo: var dic = values.ToDictionary(x => x, x => values[x]); aggiungi nuovi valori al dizionario, quindi passalo al costruttore di FormUrlEncodedContente chiamalo ReadAsStringAsync().Result. Questo ti dà una stringa di query codificata correttamente, che puoi assegnare di nuovo a UriBuilder.
Triynko,

In realtà può semplicemente utilizzare NamedValueCollection.ToString invece di tutto questo, ma solo se si cambia un app.config / web.config impostazione che impedisce ASP.NET da utilizzando il '% uXXXX' encoding: <add key="aspnet:DontUsePercentUUrlEncoding" value="true" />. Non vorrei dipendere da questo comportamento, quindi è meglio utilizzare la classe FormUrlEncodedContent, come dimostrato da una precedente risposta: stackoverflow.com/a/26744471/88409
Triynko

41

In un progetto ASP.NET Core è possibile utilizzare la 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));

2
È fastidioso che sebbene con questo processo non sia ancora possibile inviare più valori per la stessa chiave. Se si desidera inviare "bar" e "bar2" come parte di pippo, non è possibile.
m0g

2
Questa è un'ottima risposta per le app moderne, funziona nel mio scenario, semplice e pulita. Tuttavia, non ho bisogno di alcun meccanismo di fuga - non testato.
Patrick Stalph,

Questo pacchetto NuGet si rivolge a .NET standard 2.0, il che significa che è possibile utilizzarlo su .NET framework 4.6.1+
eddiewould

24

Potresti voler dare un'occhiata a Flurl [divulgazione: sono l'autore], un generatore di URL fluente con lib complementare opzionale che lo estende in un client 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>();

Consulta i documenti per maggiori dettagli. Il pacchetto completo è disponibile su NuGet:

PM> Install-Package Flurl.Http

o solo il generatore di URL autonomo:

PM> Install-Package Flurl


2
Perché non estendere Urio iniziare con la tua classe anziché string?
mpen

2
Tecnicamente ho iniziato con la mia Urlclasse. Quanto sopra equivale a new Url("https://api.com").AppendPathSegment...Personalmente preferisco le estensioni di stringa a causa di un minor numero di tasti e standardizzate su di esse nei documenti, ma puoi farlo in entrambi i modi.
Todd Menier,

Fuori tema, ma lib davvero bella, lo sto usando dopo aver visto questo. Grazie per aver utilizzato anche IHttpClientFactory.
Ed S.

4

Sulla stessa linea come il post di Rostov, se non si desidera includere un riferimento a System.Webnel progetto, è possibile utilizzare FormDataCollectionda System.Net.Http.Formattinge fare qualcosa di simile alla seguente:

utilizzando 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();

3

Darin offrì una soluzione interessante e intelligente, ed ecco qualcosa che potrebbe essere un'altra opzione:

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 quindi quando lo usi, potresti farlo:

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

var url = ...
url += "?" + parms;

5
Si desidera codificare kvp.Keye kvp.Valueseparatamente all'interno del ciclo for, non nella stringa di query completa (quindi non codificare i caratteri &e =).
Matteo,

Grazie Mike. Le altre soluzioni proposte (che coinvolgono NameValueCollection) non hanno funzionato per me perché sono in un progetto PCL, quindi questa è stata un'alternativa perfetta. Per gli altri che stanno lavorando sul lato client, server.UrlEncodepuò essere sostituito conWebUtility.UrlEncode
BCA

2

O semplicemente usando la mia estensione Uri

Codice

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

Risultato

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


27
Non hai dimenticato la codifica URL?
Kugel,

1
questo è un ottimo esempio dell'uso delle estensioni per creare aiutanti chiari e utili. Se lo combini con la risposta accettata, sei sulla buona strada per costruire un solido RestClient
emran

2

La libreria di modelli URI RFC 6570 che sto sviluppando è in grado di eseguire questa operazione. Tutta la codifica viene gestita per te in conformità a tale RFC. Al momento della stesura di questo documento, è disponibile una versione beta e l'unica ragione per cui non è considerata una versione 1.0 stabile è che la documentazione non soddisfa pienamente le mie aspettative (vedere numeri # 17 , # 18 , # 32 , # 43 ).

Puoi creare una sola stringa di query:

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

Oppure potresti creare un 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);

1

Dal momento che devo riutilizzare questa volta, ho ideato questa classe che aiuta semplicemente ad astrarre il modo in cui è composta la stringa di query.

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

}

L'uso sarà semplificato a qualcosa del genere:

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

che restituirà l'uri: http://example.com/?foo=bar%3c%3e%26-baz&bar=second


1

Per evitare il doppio problema di codifica descritto nella risposta di taras.roshko e per mantenere la possibilità di lavorare facilmente con i parametri della query, è possibile utilizzare uriBuilder.Uri.ParseQueryString()invece di HttpUtility.ParseQueryString().


1

Buona parte della risposta accettata, modificata per utilizzare UriBuilder.Uri.ParseQueryString () invece di 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();

Cordiali saluti: Ciò richiede un riferimento a System.Net.Http poiché il ParseQueryString()metodo di estensione non è compreso System.
Sunny Patel,

0

Grazie a "Darin Dimitrov", questo è il metodo di estensione.

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

-1

Non sono riuscito a trovare una soluzione migliore della creazione di un metodo di estensione per convertire un dizionario in QueryStringFormat. Anche la soluzione proposta da Waleed AK è buona.

Segui la mia soluzione:

Crea il metodo di estensione:

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 loro:

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"

1
A questa soluzione manca la codifica URL corretta dei parametri e non funziona con valori che contengono caratteri "non validi"
Xavier Poinas

Sentiti libero di aggiornare la risposta e aggiungere la riga di codifica mancante, è solo una riga di codice!
Diego Mendes,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.