Zbuduj ciąg zapytania dla System.Net.HttpClient get

184

Jeśli chcę przesłać żądanie HTTP get za pomocą System.Net.HttpClient, wydaje się, że nie ma interfejsu API do dodawania parametrów, czy to prawda?

Czy jest dostępny jakiś prosty interfejs API do zbudowania ciągu zapytania, który nie wymaga zbudowania kolekcji wartości nazw i adresu URL kodującego je, a następnie ich konkatenacji? Miałem nadzieję użyć czegoś takiego jak API RestSharp (tj. AddParameter (..))

NeoDarque
źródło
@Michael Perrenoud możesz ponownie rozważyć użycie przyjętej odpowiedzi ze znakami, które wymagają kodowania, zobacz moje wyjaśnienie poniżej
nielegalny-imigrant

Odpowiedzi:

309

Jeśli chcę przesłać żądanie HTTP get za pomocą System.Net.HttpClient, wydaje się, że nie ma interfejsu API do dodawania parametrów, czy to prawda?

Tak.

Czy jest dostępny jakiś prosty interfejs API do zbudowania ciągu zapytania, który nie wymaga zbudowania kolekcji wartości nazw i adresu URL kodującego je, a następnie ich konkatenacji?

Pewnie:

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

da ci oczekiwany wynik:

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

Może się również UriBuilderprzydać klasa:

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

da ci oczekiwany wynik:

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

że możesz bardziej niż bezpiecznie karmić swoją HttpClient.GetAsyncmetodę.

Darin Dimitrov
źródło
9
To absolutnie najlepsze pod względem obsługi adresów URL w .NET. Nie ma potrzeby ręcznego kodowania adresu URL i łączenia łańcuchów, konstruktorów łańcuchów ani nic takiego. Klasa UriBuilder obsłuży nawet adresy URL z fragmentami ( #) za pomocą właściwości Fragment. Widziałem tak wielu ludzi, którzy popełniają błąd polegający na ręcznym obsługiwaniu adresów URL zamiast korzystania z wbudowanych narzędzi.
Darin Dimitrov
6
NameValueCollection.ToString()normalnie nie tworzy ciągów zapytań i nie ma dokumentacji stwierdzającej, że wykonanie ToStringwyniku ParseQueryStringspowoduje powstanie nowego ciągu zapytań, a zatem może zostać zerwane w dowolnym momencie, ponieważ nie ma gwarancji na tę funkcjonalność.
Matthew
11
HttpUtility znajduje się w System.Web, który nie jest dostępny w przenośnym środowisku uruchomieniowym. Wydaje się dziwne, że ta funkcja nie jest ogólnie dostępna w bibliotekach klas.
Chris Eldredge
82
To rozwiązanie jest nikczemne. .Net powinien mieć odpowiedni konstruktor kwerend.
Kugel
8
Fakt, że najlepsze rozwiązanie jest ukryte w klasie wewnętrznej, do której można się dostać tylko przez wywołanie metody narzędzia przekazującej pusty ciąg, nie może być dokładnie nazwany eleganckim rozwiązaniem.
Kugel
79

Dla tych, którzy nie chcą zawierać System.Webw projektach, które nie korzystają już go można użyć FormUrlEncodedContentod System.Net.Httpi zrobić coś jak poniżej:

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

wersja słownikowa

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;
}
Rostów
źródło
Dlaczego używasz instrukcji using?
Ian Warburton
Prawdopodobnie zwalnia zasoby, ale to przesada. Nie rób tego
Kody
5
Może być to bardziej zwięzłe, używając Dictionary <string, string> zamiast tablicy KVP. Następnie przy użyciu składni inicjalizującej: {„ham”, „Glazed?” }
Sean B
@SeanB To fajny pomysł, szczególnie gdy używasz czegoś, aby dodać dynamiczną / nieznaną listę parametrów. W tym przykładzie, ponieważ jest to „stała” lista, nie czułem, że narzut słownikowy byłby tego wart.
Rostów,
6
@Kody Dlaczego mówisz, żeby nie używać dispose? Zawsze się pozbywam, chyba że mam dobry powód, aby tego nie robić, na przykład ponowne użycie HttpClient.
Dan Friedman
41

TL; DR: nie używaj zaakceptowanej wersji, ponieważ jest całkowicie zepsuta w związku z obsługą znaków Unicode i nigdy nie używaj wewnętrznego API

Znalazłem dziwny problem podwójnego kodowania z zaakceptowanym rozwiązaniem:

Jeśli więc masz do czynienia ze znakami, które muszą zostać zakodowane, zaakceptowane rozwiązanie prowadzi do podwójnego kodowania:

  • parametry zapytania są automatycznie kodowane przy użyciu NameValueCollectionindeksatora ( i używa tego UrlEncodeUnicode, nie jest to normalnie oczekiwane UrlEncode(!) )
  • Następnie po wywołaniu uriBuilder.Uritworzy nowy Uriza pomocą konstruktora, który koduje jeszcze raz (normalne kodowanie adresu URL)
  • Tego nie da się uniknąć poprzez zrobienieuriBuilder.ToString() (nawet jeśli to zwraca poprawne, Uriktóra IMO jest co najmniej niespójnością, być może błędem, ale to kolejne pytanie), a następnie za pomocą HttpClientmetody akceptującej ciąg - klient nadal tworzy Uriz przekazanego ciągu w następujący sposób:new Uri(uri, UriKind.RelativeOrAbsolute)

Małe, ale pełne 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!

Wynik:

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

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

Jak widać, bez względu na to, czy zrobisz uribuilder.ToString()+ httpClient.GetStringAsync(string)lub uriBuilder.Uri+ httpClient.GetStringAsync(Uri), ostatecznie wyślesz podwójnie zakodowany parametr

Naprawionym przykładem może być:

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

Ale używa to przestarzałego Uri konstruktora

PS w moim najnowszym .NET na Windows Server, Urikonstruktor z komentarzem bool doc mówi „przestarzały, dontEscape jest zawsze fałszywy”, ale w rzeczywistości działa zgodnie z oczekiwaniami (pomija ucieczkę)

Wygląda to na kolejny błąd ...

I nawet to jest po prostu źle - wysyła UrlEncodedUnicode do serwera, a nie tylko UrlEncoded, czego oczekuje serwer

Aktualizacja: jeszcze jedno: NameValueCollection faktycznie wykonuje UrlEncodeUnicode, którego nie należy już używać i jest niezgodny ze zwykłym url.encode / decode (patrz NameValueCollection do zapytania URL? ).

Więc sedno brzmi: nigdy nie używaj tego hacka,NameValueCollection query = HttpUtility.ParseQueryString(builder.Query); ponieważ spowoduje to bałagan parametrów zapytania unicode. Wystarczy zbudować zapytanie ręcznie i przypisać je do tego, UriBuilder.Queryktóre wykona niezbędne kodowanie, a następnie użyje Uri UriBuilder.Uri.

Doskonały przykład zranienia się za pomocą kodu, którego nie należy używać w ten sposób

nielegalny imigrant
źródło
16
Czy możesz dodać kompletną funkcję narzędzia do tej odpowiedzi, która działa?
mafu
8
Po drugie mamfu w tej sprawie: czytam odpowiedź, ale nie mam konkluzji. Czy jest na to ostateczna odpowiedź?
Richard Griffiths
3
Chciałbym również zobaczyć ostateczną odpowiedź na ten problem
Pones
Ostateczną odpowiedzią na ten problem jest użycie var namedValues = HttpUtility.ParseQueryString(builder.Query), ale zamiast użycia zwróconej NameValueCollection, natychmiast przekonwertuj ją na słownik w następujący sposób: var dic = values.ToDictionary(x => x, x => values[x]); Dodaj nowe wartości do słownika, a następnie przekaż je konstruktorowi FormUrlEncodedContenti wywołaj ReadAsStringAsync().Result. To daje poprawnie zakodowany ciąg zapytania, który możesz przypisać z powrotem do UriBuilder.
Triynko
Rzeczywiście można po prostu użyć NamedValueCollection.ToString zamiast tego wszystkiego, ale tylko wtedy, gdy zmienisz app.config / web.config ustawiania który zapobiega ASP.NET z wykorzystaniem „% uXXXX” kodowania: <add key="aspnet:DontUsePercentUUrlEncoding" value="true" />. Nie zależałbym od tego zachowania, więc lepiej jest użyć klasy FormUrlEncodedContent, jak wykazano we wcześniejszej odpowiedzi: stackoverflow.com/a/26744471/88409
Triynko
41

W projekcie ASP.NET Core można użyć klasy 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
źródło
2
To denerwujące, że mimo tego procesu nadal nie można wysłać wielu wartości dla tego samego klucza. Jeśli chcesz wysłać „bar” i „bar2” jako część foo, nie jest to możliwe.
m0g
2
To świetna odpowiedź dla nowoczesnych aplikacji, działa w moim scenariuszu, prosta i czysta. Nie potrzebuję jednak żadnych mechanizmów ucieczki - nie przetestowano.
Patrick Stalph,
Ten Nuget cele pakietu .NET 2.0 norma która oznacza, że można go używać na pełnym NET 4.6.1+
eddiewould
24

Możesz wypróbować Flurl [ujawnienie: jestem autorem], płynnego narzędzia do tworzenia adresów URL z opcjonalną biblioteką towarzyszącą, która rozszerza go na pełnoprawnego klienta REST.

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

Sprawdź dokumenty, aby uzyskać więcej informacji. Pełny pakiet jest dostępny na NuGet:

PM> Install-Package Flurl.Http

lub po prostu samodzielny program do tworzenia adresów URL:

PM> Install-Package Flurl

Todd Menier
źródło
2
Dlaczego nie rozszerzyć Urilub zacząć od własnej klasy zamiast string?
mpen
2
Technicznie zacząłem od własnej Urlklasy. Powyższe jest równoważne new Url("https://api.com").AppendPathSegment...Osobiście wolę rozszerzenia ciągów ze względu na mniejszą liczbę naciśnięć klawiszy i znormalizowane na nich w dokumentach, ale możesz to zrobić w każdy sposób.
Todd Menier
Nie na temat, ale naprawdę fajna biblioteka, używam jej po obejrzeniu. Dziękujemy za skorzystanie z IHttpClientFactory.
Ed S.
4

Według tych samych zasad jak słupka Rostowa, jeśli nie chcą zawierać odniesienie do System.Webw projekcie, można korzystać FormDataCollectionz System.Net.Http.Formattingi zrobić coś jak następuje:

Za pomocą 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
źródło
3

Darin zaoferował ciekawe i sprytne rozwiązanie, a oto coś, co może być inną opcją:

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

i podczas korzystania z niego możesz to zrobić:

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

var url = ...
url += "?" + parms;
Mike Perrenoud
źródło
5
Chciałbyś zakodować kvp.Keyi kvp.Valueosobno wewnątrz pętli for, a nie w pełnym ciągu zapytania (tym samym nie kodując znaków &i =).
Matthew
Dzięki Mike. Inne proponowane rozwiązania (obejmujące NameValueCollection) nie działały dla mnie, ponieważ jestem w projekcie PCL, więc była to idealna alternatywa. Dla innych, którzy pracują po stronie klienta, server.UrlEncodemożna je zastąpićWebUtility.UrlEncode
BCA
2

Lub po prostu używając mojego rozszerzenia Uri

Kod

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

Stosowanie

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

Wynik

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

Roman Ratskey
źródło
27
Nie zapomniałeś kodowania adresów URL?
Kugel
1
to świetny przykład używania rozszerzeń do tworzenia jasnych, przydatnych pomocników. Jeśli połączysz to z zaakceptowaną odpowiedzią, jesteś na dobrej drodze do zbudowania solidnego RestClient
emran
2

RFC 6570 URI Template Library Zajmuję jest zdolny do wykonywania tej operacji. Wszystkie kodowania są obsługiwane zgodnie z RFC. W chwili pisania tego tekstu dostępna jest wersja beta, a jedynym powodem, dla którego nie jest uważana za stabilną wersję 1.0, jest to, że dokumentacja nie w pełni spełnia moje oczekiwania (patrz numery 17 , 18 , 32 , 43 ).

Możesz zbudować ciąg zapytania sam:

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

Lub możesz zbudować pełny URI:

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
źródło
1

Ponieważ muszę użyć tego czasu kilka razy, wymyśliłem tę klasę, która po prostu pomaga wyodrębnić sposób tworzenia ciągu zapytania.

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

}

Użycie zostanie uproszczone do czegoś takiego:

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

, który zwróci identyfikator: http://example.com/?foo=bar%3c%3e%26-baz&bar=second

Jaider
źródło
1

Aby uniknąć problemu podwójnego kodowania opisanego w odpowiedzi na taras.roshko i zachować możliwość łatwej pracy z parametrami zapytania, możesz użyć uriBuilder.Uri.ParseQueryString()zamiast HttpUtility.ParseQueryString().

Valeriy Lyuchyn
źródło
1

Dobra część zaakceptowanej odpowiedzi, zmodyfikowana do użycia UriBuilder.Uri.ParseQueryString () zamiast 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
źródło
FYI: Wymaga to odwołania do System.Net.Http, ponieważ ParseQueryString()metoda rozszerzenia nie istnieje System.
Sunny Patel
0

Dzięki „Darin Dimitrov”, To są metody rozszerzenia.

 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
źródło
-1

Nie mogłem znaleźć lepszego rozwiązania niż stworzenie metody rozszerzenia do konwersji słownika na QueryStringFormat. Rozwiązanie zaproponowane przez Waleed AK jest również dobre.

Postępuj zgodnie z moim rozwiązaniem:

Utwórz metodę rozszerzenia:

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

I oni:

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
źródło
1
W tym rozwiązaniu brakuje właściwego kodowania parametrów URL i nie będzie działać z wartościami zawierającymi „nieprawidłowe” znaki
Xavier Poinas,
Zaktualizuj odpowiedź i dodaj brakującą linię kodowania, to tylko linia kodu!
Diego Mendes,