Bangun string kueri untuk dapatkan System.Net.HttpClient

184

Jika saya ingin mengirimkan permintaan dapatkan http menggunakan System.Net.HttpClient tampaknya tidak ada api untuk menambahkan parameter, apakah ini benar?

Apakah ada api sederhana yang tersedia untuk membangun string kueri yang tidak melibatkan pembuatan koleksi nilai nama dan url yang mengkodekannya dan akhirnya menggabungkannya? Saya berharap untuk menggunakan sesuatu seperti api RestSharp (yaitu AddParameter (..))

NeoDarque
sumber
@Michael Perrenoud Anda mungkin ingin mempertimbangkan kembali menggunakan jawaban yang diterima dengan karakter yang perlu penyandian, lihat penjelasan saya di bawah ini
ilegal-imigran

Jawaban:

309

Jika saya ingin mengirimkan permintaan dapatkan http menggunakan System.Net.HttpClient tampaknya tidak ada api untuk menambahkan parameter, apakah ini benar?

Iya.

Apakah ada api sederhana yang tersedia untuk membangun string kueri yang tidak melibatkan pembuatan koleksi nilai nama dan url yang mengkodekannya dan akhirnya menggabungkannya?

Tentu:

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

akan memberi Anda hasil yang diharapkan:

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

Anda mungkin juga menemukan UriBuilderkelas yang berguna:

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

akan memberi Anda hasil yang diharapkan:

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

Anda bisa memberi makan lebih aman ke HttpClient.GetAsyncmetode Anda .

Darin Dimitrov
sumber
9
Itu yang terbaik dalam hal penanganan url di .NET. Tidak perlu pernah secara manual menyandikan url dan melakukan penggabungan string atau pembangun string atau apa pun. Kelas UriBuilder bahkan akan menangani url dengan fragmen ( #) untuk Anda menggunakan properti Fragmen. Saya telah melihat begitu banyak orang melakukan kesalahan dalam menangani url secara manual alih-alih menggunakan alat bawaan.
Darin Dimitrov
6
NameValueCollection.ToString()biasanya tidak membuat string kueri, dan tidak ada dokumentasi yang menyatakan bahwa melakukan ToStringpada hasil ParseQueryStringakan menghasilkan string kueri baru, sehingga bisa pecah kapan saja karena tidak ada jaminan dalam fungsionalitas itu.
Matius
11
HttpUtility ada di System.Web yang tidak tersedia pada runtime portabel. Tampaknya aneh bahwa fungsi ini tidak tersedia secara umum di perpustakaan kelas.
Chris Eldredge
82
Solusi ini tercela. .Net harus memiliki pembangun querystring yang tepat.
Kugel
8
Fakta bahwa solusi terbaik disembunyikan di kelas internal yang hanya bisa Anda dapatkan dengan memanggil metode utilitas lewat string kosong tidak bisa disebut solusi elegan.
Kugel
79

Bagi mereka yang tidak ingin memasukkan System.Webdalam proyek yang belum menggunakannya, Anda dapat menggunakan FormUrlEncodedContentdari System.Net.Httpdan melakukan sesuatu seperti berikut:

versi kunciperbaikan kunci

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

versi kamus

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
sumber
Mengapa Anda menggunakan pernyataan menggunakan?
Ian Warburton
Mungkin untuk membebaskan sumber daya, tetapi ini terlalu berlebihan. Jangan lakukan ini.
Kody
5
Ini bisa lebih ringkas dengan menggunakan Kamus <string, string> daripada array KVP. Kemudian gunakan sintaksis initializer dari: {"ham", "Glazed?" }
Sean B
@ SeanB Itu ide yang bagus, terutama ketika menggunakan sesuatu untuk menambahkan daftar parameter dinamis / tidak diketahui. Untuk contoh ini karena ini adalah daftar "tetap", saya tidak merasa biaya overhead kamus sepadan.
Rostov
6
@Kody Mengapa Anda mengatakan untuk tidak menggunakan dispose? Saya selalu membuang kecuali saya memiliki alasan yang baik untuk tidak melakukannya, seperti menggunakan kembali HttpClient.
Dan Friedman
41

TL; DR: jangan gunakan versi yang diterima karena itu benar-benar rusak dalam kaitannya dengan penanganan karakter unicode, dan tidak pernah menggunakan API internal

Saya benar-benar menemukan masalah penyandian ganda yang aneh dengan solusi yang diterima:

Jadi, Jika Anda berurusan dengan karakter yang perlu disandikan, solusi yang diterima mengarah ke penyandian ganda:

  • parameter kueri dikodekan secara otomatis dengan menggunakan NameValueCollectionpengindeks ( dan ini menggunakan UrlEncodeUnicode, tidak diharapkan biasa UrlEncode(!) )
  • Kemudian, ketika Anda memanggilnya uriBuilder.Urimembuat Urikonstruktor menggunakan baru yang melakukan pengkodean sekali lagi (pengodean url normal)
  • Itu tidak dapat dihindari dengan melakukanuriBuilder.ToString() (meskipun ini mengembalikan benar UriIMO mana yang setidaknya tidak konsisten, mungkin bug, tapi itu pertanyaan lain) dan kemudian menggunakan HttpClientmetode menerima string - klien masih membuat Urikeluar dari string yang Anda kirimkan seperti ini:new Uri(uri, UriKind.RelativeOrAbsolute)

Repro kecil, tetapi penuh:

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!

Keluaran:

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

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

Seperti yang Anda lihat, tidak masalah jika Anda melakukan uribuilder.ToString()+ httpClient.GetStringAsync(string)atau uriBuilder.Uri+ httpClient.GetStringAsync(Uri)Anda akhirnya mengirim parameter ganda yang dikodekan

Contoh tetap dapat:

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

Tapi ini menggunakan konstruktor usang Uri

PS pada .NET terbaru saya di Windows Server, Urikonstruktor dengan komentar bool doc mengatakan "usang, dontEscape selalu salah", tetapi sebenarnya berfungsi seperti yang diharapkan (melompat keluar)

Jadi sepertinya ada bug lain ...

Dan bahkan ini jelas salah - ia mengirim UrlEncodedUnicode ke server, bukan hanya UrlEncoded yang diharapkan server

Pembaruan: satu hal lagi adalah, NameValueCollection sebenarnya melakukan UrlEncodeUnicode, yang tidak seharusnya digunakan lagi dan tidak kompatibel dengan url.encode / decode biasa (lihat NameValueCollection to URL Query? ).

Jadi intinya adalah: jangan pernah menggunakan hack iniNameValueCollection query = HttpUtility.ParseQueryString(builder.Query); karena akan mengacaukan parameter permintaan unicode Anda. Cukup buat query secara manual dan tetapkan ke UriBuilder.Queryyang akan melakukan pengkodean yang diperlukan dan kemudian gunakan Uri UriBuilder.Uri.

Contoh utama menyakiti diri sendiri dengan menggunakan kode yang tidak seharusnya digunakan seperti ini

imigran ilegal
sumber
16
Bisakah Anda menambahkan fungsi utilitas lengkap untuk jawaban ini yang berfungsi?
mafu
8
Saya mafu kedua tentang ini: Saya membaca jawabannya tetapi tidak punya kesimpulan. Apakah ada jawaban pasti untuk ini?
Richard Griffiths
3
Saya juga ingin melihat jawaban pasti untuk masalah ini
Pones
Jawaban pasti untuk masalah ini adalah menggunakan var namedValues = HttpUtility.ParseQueryString(builder.Query), tetapi alih-alih menggunakan NameValueCollection yang dikembalikan, segera konversikan ke Kamus seperti: var dic = values.ToDictionary(x => x, x => values[x]); Tambahkan nilai baru ke kamus, lalu meneruskannya ke konstruktor FormUrlEncodedContentdan panggil ReadAsStringAsync().Result. Itu memberi Anda string kueri yang disandikan dengan benar, yang dapat Anda tetapkan kembali ke UriBuilder.
Triynko
Anda benar-benar dapat hanya menggunakan NamedValueCollection.ToString bukan semua itu, tetapi hanya jika Anda mengubah app.config / web.config pengaturan yang mencegah ASP.NET dari menggunakan '% uxxxx' encoding: <add key="aspnet:DontUsePercentUUrlEncoding" value="true" />. Saya tidak akan bergantung pada perilaku ini, jadi lebih baik menggunakan kelas FormUrlEncodedContent, seperti yang ditunjukkan oleh jawaban sebelumnya: stackoverflow.com/a/26744471/88409
Triynko
41

Dalam proyek ASP.NET Core Anda bisa menggunakan kelas 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
sumber
2
Itu menjengkelkan bahwa meskipun dengan proses ini Anda masih tidak dapat mengirim beberapa nilai untuk kunci yang sama. Jika Anda ingin mengirim "bar" dan "bar2" sebagai bagian dari just foo, itu tidak mungkin.
m0g
2
Ini adalah jawaban yang bagus untuk aplikasi modern, berfungsi dalam skenario saya, sederhana dan bersih. Namun, saya tidak memerlukan mekanisme melarikan diri - tidak diuji.
Patrick Stalph
Paket NuGet ini menargetkan .NET standar 2.0 yang berarti Anda dapat menggunakannya pada .NET framework 4.6.1+ lengkap
eddiewould
24

Anda mungkin ingin memeriksa Flurl [pengungkapan: Saya penulisnya], pembuat URL yang lancar dengan lib pengiring opsional yang memperluasnya menjadi klien REST yang lengkap.

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

Lihat dokumen untuk detail lebih lanjut. Paket lengkap tersedia di NuGet:

PM> Install-Package Flurl.Http

atau hanya pembuat URL yang berdiri sendiri:

PM> Install-Package Flurl

Todd Menier
sumber
2
Mengapa tidak memperluas Uriatau mulai dengan kelas Anda sendiri string?
mpen
2
Secara teknis saya mulai dengan Urlkelas saya sendiri . Di atas setara dengan new Url("https://api.com").AppendPathSegment...Secara Pribadi saya lebih suka ekstensi string karena penekanan tombol lebih sedikit dan standar pada mereka dalam dokumen, tetapi Anda bisa melakukannya dengan cara baik.
Todd Menier
Di luar topik, tetapi Lib sangat bagus, saya menggunakannya setelah melihat ini. Terima kasih telah menggunakan IHttpClientFactory juga.
Ed S.
4

Sepanjang baris yang sama dengan posting Rostov, jika Anda tidak ingin memasukkan referensi ke System.Webdalam proyek Anda, Anda dapat menggunakan FormDataCollectiondari System.Net.Http.Formattingdan melakukan sesuatu seperti berikut:

Menggunakan 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();
cwill
sumber
3

Darin menawarkan solusi yang menarik dan cerdas, dan ini adalah sesuatu yang mungkin menjadi pilihan lain:

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

dan saat menggunakannya, Anda dapat melakukan ini:

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

var url = ...
url += "?" + parms;
Mike Perrenoud
sumber
5
Anda ingin menyandikan kvp.Keydan kvp.Valuesecara terpisah di dalam for loop, bukan dalam string-kueri penuh (sehingga tidak encoding &, dan =karakter).
Matius
Terima kasih Mike. Solusi yang diusulkan lainnya (melibatkan NameValueCollection) tidak bekerja untuk saya karena saya sedang dalam proyek PCL, jadi ini adalah alternatif yang sempurna. Bagi yang lain yang bekerja di sisi klien, server.UrlEncodedapat diganti denganWebUtility.UrlEncode
BCA
2

Atau cukup menggunakan ekstensi Uri saya

Kode

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

Pemakaian

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

Hasil

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

Roman Ratskey
sumber
27
Tidakkah Anda lupa penyandian URL?
Kugel
1
ini adalah contoh bagus menggunakan ekstensi untuk membuat pembantu yang jelas dan bermanfaat. Jika Anda menggabungkan ini dengan jawaban yang diterima Anda sedang dalam perjalanan untuk membangun RestClient yang solid
emran
2

The RFC 6570 URI Template perpustakaan Aku sedang mengembangkan mampu melakukan operasi ini. Semua pengodean ditangani untuk Anda sesuai dengan RFC itu. Pada saat penulisan ini, rilis beta tersedia dan satu-satunya alasan itu tidak dianggap rilis stabil 1.0 adalah dokumentasi tidak sepenuhnya memenuhi harapan saya (lihat masalah # 17 , # 18 , # 32 , # 43 ).

Anda bisa membuat string kueri sendirian:

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

Atau Anda dapat membangun URI lengkap:

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

Karena saya harus menggunakan kembali beberapa waktu ini, saya datang dengan kelas ini yang hanya membantu mengabstraksi bagaimana string kueri disusun.

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

}

Penggunaannya akan disederhanakan menjadi seperti ini:

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

yang akan mengembalikan uri: http://example.com/?foo=bar%3c%3e%26-baz&bar=second

Jaider
sumber
1

Untuk menghindari masalah pengodean ganda yang dijelaskan dalam jawaban taras.roshko dan untuk menjaga kemungkinan agar mudah bekerja dengan parameter kueri, Anda bisa menggunakan uriBuilder.Uri.ParseQueryString()alih-alih HttpUtility.ParseQueryString().

Valeriy Lyuchyn
sumber
1

Sebagian baik dari jawaban yang diterima, dimodifikasi untuk menggunakan UriBuilder.Uri.ParseQueryString () alih-alih 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
sumber
FYI: Ini membutuhkan referensi ke System.Net.Http karena ParseQueryString()metode ekstensi tidak ada System.
Sunny Patel
0

Berkat "Darin Dimitrov", Ini adalah metode ekstensi.

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

Saya tidak dapat menemukan solusi yang lebih baik daripada membuat metode ekstensi untuk mengonversi Kamus ke QueryStringFormat. Solusi yang diajukan oleh Waleed AK juga bagus.

Ikuti solusi saya:

Buat metode ekstensi:

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

Dan mereka:

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
sumber
1
Solusi ini tidak memiliki URL penyandian parameter yang benar dan tidak akan berfungsi dengan nilai-nilai yang mengandung karakter 'tidak valid'
Xavier Poinas
Jangan ragu untuk memperbarui jawabannya dan menambahkan baris penyandian yang hilang, itu hanya sebaris kode!
Diego Mendes