Erstellen Sie eine Abfragezeichenfolge für System.Net.HttpClient get

184

Wenn ich eine http get-Anfrage mit System.Net.HttpClient senden möchte, scheint es keine API zum Hinzufügen von Parametern zu geben. Ist dies korrekt?

Gibt es eine einfache API zum Erstellen der Abfragezeichenfolge, bei der keine Namenswertsammlung und URL-Codierung erstellt und diese schließlich verkettet werden? Ich hatte gehofft, so etwas wie RestSharps API zu verwenden (dh AddParameter (..))

NeoDarque
quelle
@ Michael Perrenoud Sie möchten vielleicht die Verwendung der akzeptierten Antwort mit Zeichen, die codiert werden müssen, überdenken, siehe meine Erklärung unten
illegaler Einwanderer

Antworten:

308

Wenn ich eine http get-Anfrage mit System.Net.HttpClient senden möchte, scheint es keine API zum Hinzufügen von Parametern zu geben. Ist dies korrekt?

Ja.

Gibt es eine einfache API zum Erstellen der Abfragezeichenfolge, bei der keine Namenswertsammlung und URL-Codierung erstellt und diese schließlich verkettet werden?

Sicher:

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

gibt Ihnen das erwartete Ergebnis:

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

Möglicherweise finden Sie die UriBuilderKlasse auch nützlich:

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

gibt Ihnen das erwartete Ergebnis:

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

dass Sie Ihre HttpClient.GetAsyncMethode mehr als sicher füttern könnten .

Darin Dimitrov
quelle
9
Das ist das absolut Beste in Bezug auf die URL-Verarbeitung in .NET. Sie müssen niemals manuell eine URL-Codierung durchführen und String-Verkettungen oder String-Builder oder was auch immer ausführen. Die UriBuilder-Klasse verarbeitet #mithilfe der Fragment-Eigenschaft sogar URLs mit fragment ( ) für Sie. Ich habe so viele Leute gesehen, die den Fehler gemacht haben, URLs manuell zu verarbeiten, anstatt die integrierten Tools zu verwenden.
Darin Dimitrov
6
NameValueCollection.ToString()Normalerweise werden keine Abfragezeichenfolgen erstellt, und es gibt keine Dokumentation, die besagt, dass das Ausführen eines ToStringErgebnisses von ParseQueryStringzu einer neuen Abfragezeichenfolge führt und daher jederzeit unterbrochen werden kann, da für diese Funktionalität keine Garantie besteht.
Matthew
11
HttpUtility befindet sich in System.Web, das zur portablen Laufzeit nicht verfügbar ist. Es scheint seltsam, dass diese Funktionalität in den Klassenbibliotheken nicht allgemeiner verfügbar ist.
Chris Eldredge
82
Diese Lösung ist verabscheuungswürdig. .Net sollte einen geeigneten Querystring-Builder haben.
Kugel
8
Die Tatsache, dass die beste Lösung in der internen Klasse verborgen ist, zu der Sie nur durch Aufrufen einer Dienstprogrammmethode gelangen können, die eine leere Zeichenfolge übergibt, kann nicht genau als elegante Lösung bezeichnet werden.
Kugel
79

Für diejenigen , die nicht berücksichtigt werden sollen System.Webnicht bereits in Projekte , die Sie verwenden, können Sie FormUrlEncodedContentaus System.Net.Httpund etwas tun , wie folgt aus :

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

Wörterbuchversion

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;
}
Rostow
quelle
Warum verwenden Sie eine using-Anweisung?
Ian Warburton
Wahrscheinlich Ressourcen freisetzen, aber das ist übertrieben. Tu das nicht.
Kody
5
Dies kann präziser sein, wenn Dictionary <string, string> anstelle des KVP-Arrays verwendet wird. Verwenden Sie dann die Initialisierersyntax von: {"ham", "Glazed?" }
Sean B
@SeanB Das ist eine gute Idee, besonders wenn Sie etwas verwenden, um eine dynamische / unbekannte Liste von Parametern hinzuzufügen. In diesem Beispiel hatte ich nicht das Gefühl, dass sich der Aufwand für ein Wörterbuch gelohnt hat, da es sich um eine "feste" Liste handelt.
Rostow
6
@Kody Warum sagst du, nicht zu verwenden dispose? Ich verfüge immer, es sei denn, ich habe einen guten Grund, es nicht wiederzuverwenden HttpClient.
Dan Friedman
41

TL; DR: Verwenden Sie keine akzeptierte Version, da diese in Bezug auf die Behandlung von Unicode-Zeichen völlig fehlerhaft ist, und verwenden Sie niemals die interne API

Ich habe tatsächlich ein seltsames Problem mit der doppelten Codierung bei der akzeptierten Lösung festgestellt:

Wenn Sie also mit Zeichen arbeiten, die codiert werden müssen, führt eine akzeptierte Lösung zu einer doppelten Codierung:

  • Abfrageparameter werden mithilfe des NameValueCollectionIndexers automatisch codiert ( und dies verwendet UrlEncodeUnicodenicht regelmäßig erwartet UrlEncode(!) ).
  • Wenn Sie dann aufrufen uriBuilder.Uri, wird ein neuer UriKonstruktor erstellt, der die Codierung noch einmal ausführt (normale URL-Codierung).
  • uriBuilder.ToString()Dies kann nicht vermieden werden, indem Sie (obwohl dies korrekt zurückgibt, Uriwelche IMO zumindest inkonsistent ist, möglicherweise ein Fehler, aber das ist eine andere Frage) und dann eine HttpClientMethode verwenden, die eine Zeichenfolge akzeptiert - der Client erstellt immer noch Uriaus Ihrer übergebenen Zeichenfolge Folgendes:new Uri(uri, UriKind.RelativeOrAbsolute)

Kleiner, aber voller Repro:

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!

Ausgabe:

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

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

Wie Sie vielleicht sehen, egal ob Sie uribuilder.ToString()+ httpClient.GetStringAsync(string)oder uriBuilder.Uri+ tunhttpClient.GetStringAsync(Uri) senden Sie , doppelt codierte Parameter

Ein behobenes Beispiel könnte sein:

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

Dies verwendet jedoch einen veralteten Uri Konstruktor

PS in meinem neuesten .NET unter Windows Server, UriKonstruktor mit bool doc Kommentar sagt "veraltet, dontEscape ist immer falsch", funktioniert aber tatsächlich wie erwartet (überspringt das Escapezeichen)

Es sieht also nach einem weiteren Fehler aus ...

Und selbst das ist einfach falsch - es sendet UrlEncodedUnicode an den Server, nicht nur UrlEncoded, was der Server erwartet

Update: Eine weitere Sache ist, dass NameValueCollection tatsächlich UrlEncodeUnicode ausführt, das nicht mehr verwendet werden soll und nicht mit regulärem url.encode / decode kompatibel ist (siehe NameValueCollection to URL Query? ).

Das Fazit lautet also: Verwenden Sie diesen Hack niemals,NameValueCollection query = HttpUtility.ParseQueryString(builder.Query); da dies Ihre Unicode-Abfrageparameter durcheinander bringt. Erstellen Sie die Abfrage einfach manuell und weisen Sie sie UriBuilder.Queryder erforderlichen Codierung zu, und verwenden Sie dann UriUriBuilder.Uri .

Paradebeispiel dafür, wie Sie sich selbst verletzen, indem Sie Code verwenden, der nicht so verwendet werden soll

illegaler Einwanderer
quelle
16
Könnten Sie dieser Antwort eine vollständige Dienstprogrammfunktion hinzufügen, die funktioniert?
Mafu
8
Ich bin der zweite Mafu: Ich habe die Antwort durchgelesen, aber keine Schlussfolgerung gezogen. Gibt es eine endgültige Antwort darauf?
Richard Griffiths
3
Ich würde auch gerne die endgültige Antwort für dieses Problem sehen
Pones
Die endgültige Antwort auf dieses Problem ist die Verwendung var namedValues = HttpUtility.ParseQueryString(builder.Query), aber anstatt die zurückgegebene NameValueCollection zu verwenden, konvertieren Sie sie sofort in ein Wörterbuch wie folgt: var dic = values.ToDictionary(x => x, x => values[x]); Fügen Sie dem Wörterbuch neue Werte hinzu, übergeben Sie es an den Konstruktor von FormUrlEncodedContentund rufen Sie ReadAsStringAsync().Resultes auf. Dadurch erhalten Sie eine ordnungsgemäß codierte Abfragezeichenfolge, die Sie dem UriBuilder zurückweisen können.
Triynko
Sie können stattdessen nur NamedValueCollection.ToString verwenden, aber nur, wenn Sie eine Einstellung für app.config / web.config ändern, die verhindert, dass ASP.NET die Codierung '% uXXXX' verwendet : <add key="aspnet:DontUsePercentUUrlEncoding" value="true" />. Ich würde mich nicht auf dieses Verhalten verlassen, daher ist es besser, die FormUrlEncodedContent-Klasse zu verwenden, wie eine frühere Antwort zeigt: stackoverflow.com/a/26744471/88409
Triynko
40

In einem ASP.NET Core-Projekt können Sie die QueryHelpers-Klasse verwenden.

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

var response = await client.GetAsync(QueryHelpers.AddQueryString("/api/", query));
Magu
quelle
2
Es ist ärgerlich, dass Sie mit diesem Vorgang immer noch nicht mehrere Werte für denselben Schlüssel senden können. Wenn Sie "bar" und "bar2" als Teil von "nur foo" senden möchten, ist dies nicht möglich.
m0g
2
Dies ist eine großartige Antwort für moderne Apps, funktioniert in meinem Szenario einfach und sauber. Ich brauche jedoch keine Fluchtmechanismen - nicht getestet.
Patrick Stalph
Dieses NuGet-Paket zielt auf .NET Standard 2.0 ab, was bedeutet, dass Sie es auf dem vollständigen .NET Framework 4.6.1+ verwenden können
eddiewould
24

Vielleicht möchten Sie sich Flurl [Offenlegung: Ich bin der Autor] ansehen , einen fließenden URL-Builder mit optionaler Companion-Bibliothek, der ihn zu einem vollständigen REST-Client erweitert.

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

Auschecken Weitere Informationen finden den Dokumenten . Das vollständige Paket ist auf NuGet verfügbar:

PM> Install-Package Flurl.Http

oder nur der eigenständige URL-Builder:

PM> Install-Package Flurl

Todd Menier
quelle
2
Warum nicht erweitern Urioder mit Ihrer eigenen Klasse beginnen, anstatt string?
Mpen
2
Technisch habe ich mit meiner eigenen UrlKlasse angefangen. Das Obige entspricht new Url("https://api.com").AppendPathSegment...Persönlich. Ich bevorzuge die Zeichenfolgenerweiterungen aufgrund weniger Tastenanschläge und standardisiert in den Dokumenten, aber Sie können es so oder so tun.
Todd Menier
Off Topic, aber wirklich nette Bibliothek, ich benutze es, nachdem ich das gesehen habe. Vielen Dank, dass Sie auch IHttpClientFactory verwenden.
Ed S.
4

In Anlehnung an Rostovs Beitrag können Sie from verwenden , wenn Sie keinen Verweis auf System.Webin Ihr Projekt aufnehmen möchtenFormDataCollectionSystem.Net.Http.Formatting und etwas tun , wie folgt aus :

Verwenden von 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
quelle
3

Darin bot eine interessante und clevere Lösung an, und hier ist etwas, das eine andere Option sein könnte:

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

Wenn Sie es verwenden, können Sie Folgendes tun:

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

var url = ...
url += "?" + parms;
Mike Perrenoud
quelle
5
Sie würden kodieren wollen kvp.Keyund kvp.Valuenicht für Schleife in der separat in der vollständigen Abfrage-String (also nicht die Codierung &und =Zeichen).
Matthew
Danke Mike. Die anderen vorgeschlagenen Lösungen (mit NameValueCollection) haben bei mir nicht funktioniert, da ich in einem PCL-Projekt bin. Dies war also eine perfekte Alternative. Für andere, die auf der Client-Seite arbeiten, server.UrlEncodekann die durchWebUtility.UrlEncode
BCA
2

Oder einfach mit meiner Uri-Erweiterung

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

Verwendung

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

Ergebnis

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

Roman Ratskey
quelle
27
Haben Sie die URL-Codierung nicht vergessen?
Kugel
1
Dies ist ein großartiges Beispiel für die Verwendung von Erweiterungen, um klare und nützliche Helfer zu erstellen. Wenn Sie dies mit der akzeptierten Antwort kombinieren, sind Sie auf dem Weg, einen soliden RestClient aufzubauen
emran
2

Die von mir entwickelte RFC 6570-URI-Vorlagenbibliothek kann diesen Vorgang ausführen. Die gesamte Codierung wird gemäß diesem RFC für Sie behandelt. Zum Zeitpunkt dieses Schreibens ist eine Beta-Version verfügbar und der einzige Grund, warum sie nicht als stabile Version 1.0 angesehen wird, ist, dass die Dokumentation meine Erwartungen nicht vollständig erfüllt (siehe Probleme Nr. 17 , Nr. 18 , Nr. 32 , Nr. 43 ).

Sie können entweder nur eine Abfragezeichenfolge erstellen:

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

Oder Sie können eine vollständige URI erstellen:

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
quelle
1

Da ich diese wenigen Male wiederverwenden muss, habe ich mir diese Klasse ausgedacht, die einfach dazu beiträgt, die Zusammensetzung der Abfragezeichenfolge zu abstrahieren.

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

}

Die Verwendung wird zu so etwas vereinfacht:

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

Dadurch wird die URL zurückgegeben: http://example.com/?foo=bar%3c%3e%26-baz&bar=second

Jaider
quelle
1

Um das in der Antwort von taras.roshko beschriebene Problem der doppelten Codierung zu vermeiden und die Möglichkeit zu behalten, einfach mit Abfrageparametern zu arbeiten, können Sie uriBuilder.Uri.ParseQueryString()stattdessen verwenden HttpUtility.ParseQueryString().

Valeriy Lyuchyn
quelle
1

Ein großer Teil der akzeptierten Antwort wurde geändert, um UriBuilder.Uri.ParseQueryString () anstelle von HttpUtility.ParseQueryString () zu verwenden:

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
quelle
Zu Ihrer Information : Dies erfordert einen Verweis auf System.Net.Http, da die ParseQueryString()Erweiterungsmethode nicht enthalten ist System.
Sunny Patel
0

Dank "Darin Dimitrov" ist dies die Erweiterungsmethode.

 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
quelle
-1

Ich konnte keine bessere Lösung finden, als eine Erweiterungsmethode zum Konvertieren eines Wörterbuchs in QueryStringFormat zu erstellen. Die von Waleed AK vorgeschlagene Lösung ist ebenfalls gut.

Folgen Sie meiner Lösung:

Erstellen Sie die Erweiterungsmethode:

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

Und sie:

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
quelle
1
Diese Lösung fehlt die richtige URL-Codierung von Parametern und funktioniert nicht mit Werten, die "ungültige" Zeichen enthalten
Xavier Poinas
Fühlen Sie sich frei, die Antwort zu aktualisieren und die fehlende Codierungszeile hinzuzufügen, es ist nur eine Codezeile!
Diego Mendes