Générer une chaîne de requête pour System.Net.HttpClient get

184

Si je souhaite soumettre une requête http get en utilisant System.Net.HttpClient, il ne semble pas y avoir d'API pour ajouter des paramètres, est-ce correct?

Existe-t-il une API simple disponible pour créer la chaîne de requête qui n'implique pas la création d'une collection de valeurs de nom et un encodage d'URL, puis leur concaténation? J'espérais utiliser quelque chose comme l'api de RestSharp (c'est-à-dire AddParameter (..))

NeoDarque
la source
@Michael Perrenoud, vous voudrez peut-être reconsidérer l'utilisation de la réponse acceptée avec des caractères qui doivent être encodés, voir mon explication ci
immigrant illégal

Réponses:

309

Si je souhaite soumettre une requête http get en utilisant System.Net.HttpClient, il ne semble pas y avoir d'API pour ajouter des paramètres, est-ce correct?

Oui.

Existe-t-il une API simple disponible pour créer la chaîne de requête qui n'implique pas la création d'une collection de valeurs de nom et un encodage d'URL, puis leur concaténation?

Sûr:

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

vous donnera le résultat attendu:

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

Vous pourriez également trouver la UriBuilderclasse utile:

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

vous donnera le résultat attendu:

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

que vous pourriez plus que sans risque nourrir votre HttpClient.GetAsyncméthode.

Darin Dimitrov
la source
9
C'est le meilleur absolu en termes de gestion des URL dans .NET. Nul besoin de coder manuellement les URL et de faire des concaténations de chaînes ou des générateurs de chaînes ou autre. La classe UriBuilder gérera même les URL avec fragments ( #) pour vous en utilisant la propriété Fragment. J'ai vu tant de gens faire l'erreur de gérer manuellement les URL au lieu d'utiliser les outils intégrés.
Darin Dimitrov
6
NameValueCollection.ToString()ne crée normalement pas de chaînes de requête, et il n'y a pas de documentation indiquant que faire un ToStringsur le résultat de ParseQueryStringentraînera une nouvelle chaîne de requête, donc pourrait être interrompue à tout moment car il n'y a aucune garantie dans cette fonctionnalité.
Matthew
11
HttpUtility est dans System.Web qui n'est pas disponible sur le runtime portable. Il semble étrange que cette fonctionnalité ne soit pas plus généralement disponible dans les bibliothèques de classes.
Chris Eldredge
82
Cette solution est méprisable. .Net doit avoir un générateur de chaînes de requêtes approprié.
Kugel
8
Le fait que la meilleure solution soit cachée dans la classe interne à laquelle vous ne pouvez accéder qu'en appelant une méthode utilitaire passant une chaîne vide ne peut pas être exactement appelé une solution élégante.
Kugel
79

Pour ceux qui ne souhaitent pas l'inclure System.Webdans des projets qui ne l'utilisent pas déjà, vous pouvez utiliser FormUrlEncodedContentfrom System.Net.Httpet faire quelque chose comme ce qui suit:

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

version du dictionnaire

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
la source
Pourquoi utilisez-vous une instruction using?
Ian Warburton
Probablement de libérer des ressources, mais c'est exagéré. Ne fais pas ça.
Kody
5
Cela peut être plus concis en utilisant Dictionary <string, string> au lieu du tableau KVP. Puis en utilisant la syntaxe d'initialisation de: {"ham", "Glazed?" }
Sean B
@SeanB C'est une bonne idée, surtout lorsque vous utilisez quelque chose pour ajouter une liste de paramètres dynamiques / inconnus. Pour cet exemple, puisqu'il s'agit d'une liste "fixe", je n'ai pas l'impression que la surcharge d'un dictionnaire en valait la peine.
Rostov
6
@Kody Pourquoi dites-vous de ne pas utiliser dispose? Je jette toujours à moins d'avoir une bonne raison de ne pas le faire, comme la réutilisation HttpClient.
Dan Friedman
41

TL; DR: n'utilisez pas la version acceptée car elle est complètement cassée par rapport à la gestion des caractères unicode, et n'utilisez jamais d'API interne

J'ai en fait trouvé un problème de double encodage étrange avec la solution acceptée:

Donc, si vous avez affaire à des caractères qui doivent être encodés, la solution acceptée conduit à un double encodage:

  • les paramètres de requête sont automatiquement codés à l'aide de l' NameValueCollectionindexeur ( et cela utilise UrlEncodeUnicode, pas normalement prévu UrlEncode(!) )
  • Ensuite, lorsque vous appelez, uriBuilder.Uriil crée un nouveau Uriconstructeur utilisant qui encode une fois de plus (encodage url normal)
  • Cela ne peut pas être évité en faisanturiBuilder.ToString() (même si cela renvoie correct Uriquel IMO est au moins incohérent, peut-être un bogue, mais c'est une autre question), puis en utilisant la HttpClientméthode acceptant la chaîne - le client crée toujours à Uripartir de votre chaîne passée comme ceci:new Uri(uri, UriKind.RelativeOrAbsolute)

Petit, mais repro complet:

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!

Production:

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

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

Comme vous pouvez le voir, peu importe si vous faites uribuilder.ToString()+ httpClient.GetStringAsync(string)ou uriBuilder.Uri+ httpClient.GetStringAsync(Uri)vous finissez par envoyer un paramètre codé en double

Un exemple fixe pourrait être:

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

Mais cela utilise un Uri constructeur obsolète

PS sur mon dernier .NET sur Windows Server, le Uriconstructeur avec un commentaire booléen dit "obsolète, dontEscape est toujours faux", mais fonctionne en fait comme prévu (saute l'échappement)

Donc ça ressemble à un autre bug ...

Et même cela est tout simplement faux - il envoie UrlEncodedUnicode au serveur, pas seulement UrlEncoded ce que le serveur attend

Mise à jour: une dernière chose est que NameValueCollection fait réellement UrlEncodeUnicode, qui n'est plus censé être utilisé et est incompatible avec url.encode / décodage régulier (voir NameValueCollection to URL Query? ).

Donc, l'essentiel est: n'utilisez jamais ce hack avecNameValueCollection query = HttpUtility.ParseQueryString(builder.Query); car cela perturberait vos paramètres de requête Unicode. Créez simplement la requête manuellement et assignez-la à UriBuilder.Querylaquelle effectuera le codage nécessaire, puis obtenez Uri à l'aide de UriBuilder.Uri.

Premier exemple de vous blesser en utilisant du code qui n'est pas censé être utilisé comme ça

immigrant illégal
la source
16
Pourriez-vous ajouter une fonction utilitaire complète à cette réponse qui fonctionne?
mafu
8
Je seconde mafu sur ceci: j'ai lu la réponse mais je n'ai pas de conclusion. Y a-t-il une réponse définitive à cela?
Richard Griffiths
3
J'aimerais aussi voir la réponse définitive à ce problème
Pones
La réponse définitive à ce problème est d'utiliser var namedValues = HttpUtility.ParseQueryString(builder.Query), mais au lieu d'utiliser le NameValueCollection retourné, convertissez-le immédiatement en un dictionnaire comme ceci: var dic = values.ToDictionary(x => x, x => values[x]); Ajoutez de nouvelles valeurs au dictionnaire, puis passez-le au constructeur de FormUrlEncodedContentet appelez- ReadAsStringAsync().Resultle. Cela vous donne une chaîne de requête correctement codée, que vous pouvez réattribuer à UriBuilder.
Triynko
En fait , vous pouvez simplement utiliser NamedValueCollection.ToString au lieu de tout cela, mais seulement si vous changez un app.config / web.config paramètre qui empêche ASP.NET d'utiliser le codage « % uXXXX »: <add key="aspnet:DontUsePercentUUrlEncoding" value="true" />. Je ne dépendrais pas de ce comportement, il est donc préférable d'utiliser la classe FormUrlEncodedContent, comme le montre une réponse précédente: stackoverflow.com/a/26744471/88409
Triynko
41

Dans un projet ASP.NET Core, vous pouvez utiliser 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));
Magu
la source
2
C'est ennuyeux que, bien qu'avec ce processus, vous ne puissiez toujours pas envoyer plusieurs valeurs pour la même clé. Si vous voulez envoyer "bar" et "bar2" dans le cadre de foo uniquement, ce n'est pas possible.
m0g
2
C'est une excellente réponse pour les applications modernes, fonctionne dans mon scénario, simple et propre. Cependant, je n'ai besoin d'aucun mécanisme d'échappement - non testé.
Patrick Stalph
Ce package NuGet cible la norme .NET 2.0, ce qui signifie que vous pouvez l'utiliser sur l'ensemble du framework .NET 4.6.1+
eddiewould
24

Vous voudrez peut-être consulter Flurl [divulgation: je suis l'auteur], un constructeur d'URL fluide avec une bibliothèque compagnon en option qui l'étend en un client REST à part entière.

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

Consultez les documents pour plus de détails. Le package complet est disponible sur NuGet:

PM> Install-Package Flurl.Http

ou simplement le générateur d'URL autonome:

PM> Install-Package Flurl

Todd Menier
la source
2
Pourquoi ne pas prolonger Uriou commencer avec votre propre classe au lieu de string?
mpen le
2
Techniquement, j'ai commencé avec ma propre Urlclasse. Ce qui précède est équivalent à new Url("https://api.com").AppendPathSegment...Personnellement, je préfère les extensions de chaîne en raison du moins de frappes au clavier et standardisées sur elles dans la documentation, mais vous pouvez le faire de toute façon.
Todd Menier le
Hors sujet, mais vraiment belle lib, je l'utilise après avoir vu ça Merci d'utiliser IHttpClientFactory également.
Ed S. le
4

Dans le même esprit que l'article de Rostov, si vous ne souhaitez pas inclure de référence à System.Webdans votre projet, vous pouvez utiliser FormDataCollectionfrom System.Net.Http.Formattinget faire quelque chose comme ce qui suit:

En utilisant 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
la source
3

Darin a proposé une solution intéressante et intelligente, et voici quelque chose qui pourrait être une autre option:

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

et donc lors de son utilisation, vous pouvez faire ceci:

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

var url = ...
url += "?" + parms;
Mike Perrenoud
la source
5
Vous voudriez encoder kvp.Keyet kvp.Valueséparément dans la boucle for, pas dans la chaîne de requête complète (donc pas encoder les caractères &, et =).
Matthew
Merci Mike. Les autres solutions proposées (impliquant NameValueCollection) n'ont pas fonctionné pour moi car je suis dans un projet PCL, donc c'était une alternative parfaite. Pour les autres qui travaillent côté client, le server.UrlEncodepeut être remplacé parWebUtility.UrlEncode
BCA
2

Ou simplement en utilisant mon extension Uri

Code

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

Usage

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

Résultat

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

Ratskey romain
la source
27
N'avez-vous pas oublié l'encodage d'URL?
Kugel
1
c'est un excellent exemple d'utilisation d'extensions pour créer des aides claires et utiles. Si vous combinez cela avec la réponse acceptée, vous êtes sur la bonne voie pour créer un RestClient solide
emran
2

La bibliothèque de modèles d'URI RFC 6570 que je développe est capable d'effectuer cette opération. Tout le codage est géré pour vous conformément à cette RFC. Au moment d'écrire ces lignes, une version bêta est disponible et la seule raison pour laquelle elle n'est pas considérée comme une version 1.0 stable est que la documentation ne répond pas entièrement à mes attentes (voir les problèmes # 17 , # 18 , # 32 , # 43 ).

Vous pouvez soit créer une chaîne de requête seule:

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

Ou vous pouvez créer un URI complet:

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
la source
1

Comme je dois réutiliser cette fois-ci, j'ai créé cette classe qui aide simplement à résumer la composition de la chaîne de requête.

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'utilisation sera simplifiée à quelque chose comme ceci:

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

qui renverra l'URI: http://example.com/?foo=bar%3c%3e%26-baz&bar=second

Jaider
la source
1

Pour éviter le problème de double codage décrit dans la réponse de taras.roshko et pour garder la possibilité de travailler facilement avec les paramètres de requête, vous pouvez utiliser à la uriBuilder.Uri.ParseQueryString()place de HttpUtility.ParseQueryString().

Valeriy Lyuchyn
la source
1

Bonne partie de la réponse acceptée, modifiée pour utiliser UriBuilder.Uri.ParseQueryString () au lieu 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
la source
FYI: Cela nécessite une référence à System.Net.Http car la ParseQueryString()méthode d'extension n'est pas incluseSystem .
Sunny Patel
0

Merci à "Darin Dimitrov", ce sont les méthodes d'extension.

 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
la source
-1

Je n'ai pas pu trouver de meilleure solution que de créer une méthode d'extension pour convertir un dictionnaire en QueryStringFormat. La solution proposée par Waleed AK est également bonne.

Suivez ma solution:

Créez la méthode d'extension:

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

Et eux:

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
la source
1
Cette solution ne dispose pas d'un encodage URL correct des paramètres et ne fonctionnera pas avec des valeurs contenant des caractères `` invalides ''
Xavier Poinas
N'hésitez pas à mettre à jour la réponse et à ajouter la ligne d'encodage manquante, ce n'est qu'une ligne de code!
Diego Mendes