Xây dựng chuỗi truy vấn cho System.Net.HttpClient get


184

Nếu tôi muốn gửi một yêu cầu nhận http bằng System.Net.HttpClient dường như không có api để thêm tham số, điều này có đúng không?

Có bất kỳ api đơn giản nào có sẵn để xây dựng chuỗi truy vấn không liên quan đến việc xây dựng bộ sưu tập giá trị tên và mã hóa url và cuối cùng là nối chúng không? Tôi đã hy vọng sử dụng một cái gì đó như api của RestSharp (tức là AddParameter (..))


@Michael Perrenoud bạn có thể muốn xem xét lại bằng cách sử dụng câu trả lời được chấp nhận với các ký tự cần mã hóa, xem giải thích của tôi dưới đây
người nhập cư bất hợp pháp

Câu trả lời:


309

Nếu tôi muốn gửi một yêu cầu nhận http bằng System.Net.HttpClient dường như không có api để thêm tham số, điều này có đúng không?

Đúng.

Có bất kỳ api đơn giản nào có sẵn để xây dựng chuỗi truy vấn không liên quan đến việc xây dựng bộ sưu tập giá trị tên và mã hóa url và cuối cùng là nối chúng không?

Chắc chắn rồi:

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

sẽ cho bạn kết quả như mong đợi:

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

Bạn cũng có thể tìm thấy UriBuilderlớp hữu ích:

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

sẽ cho bạn kết quả như mong đợi:

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

rằng bạn có thể cung cấp nhiều hơn một cách an toàn cho HttpClient.GetAsyncphương pháp của bạn .


9
Đó là điều tuyệt vời nhất về mặt xử lý url trong .NET. Không cần phải mã hóa url theo cách thủ công và thực hiện nối chuỗi hoặc trình tạo chuỗi hoặc bất cứ điều gì. Lớp UriBuilder thậm chí sẽ xử lý các url với các đoạn ( #) cho bạn bằng cách sử dụng thuộc tính Fragment. Tôi đã thấy rất nhiều người mắc lỗi xử lý các url thủ công thay vì sử dụng các công cụ tích hợp.
Darin Dimitrov

6
NameValueCollection.ToString()thông thường không tạo ra các chuỗi truy vấn và không có tài liệu nào nói rằng việc thực hiện ToStringkết quả ParseQueryStringsẽ dẫn đến một chuỗi truy vấn mới, do đó có thể bị hỏng bất cứ lúc nào vì không có gì đảm bảo trong chức năng đó.
Matthew

11
HttpUtility nằm trong System.Web không có sẵn trong thời gian chạy di động. Có vẻ lạ là chức năng này thường không có sẵn trong các thư viện lớp.
Chris Eldredge

82
Giải pháp này là đáng khinh. .Net nên có trình xây dựng chuỗi truy vấn thích hợp.
Kugel

8
Thực tế là giải pháp tốt nhất được ẩn trong lớp bên trong mà bạn chỉ có thể nhận được bằng cách gọi một phương thức tiện ích chuyển qua chuỗi rỗng không thể được gọi chính xác là một giải pháp tao nhã.
Kugel

79

Đối với những người không muốn bao gồm System.Webtrong các dự án mà chưa sử dụng nó, bạn có thể sử dụng FormUrlEncodedContenttừ System.Net.Httpvà làm điều gì đó như sau:

phiên bản 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;
}

phiên bản từ điển

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

Tại sao bạn sử dụng một tuyên bố sử dụng?
Ian Warburton

Có khả năng giải phóng tài nguyên, nhưng điều này là quá đỉnh. Đừng làm điều này.
Kody

5
Điều này có thể ngắn gọn hơn bằng cách sử dụng Từ điển <chuỗi, chuỗi> thay vì mảng KVP. Sau đó, sử dụng cú pháp khởi tạo của: {"ham", "Glazed?" }
Sean B

@SeanB Đó là một ý tưởng hay, đặc biệt là khi sử dụng một cái gì đó để thêm một danh sách các tham số động / không xác định. Đối với ví dụ này vì đây là danh sách "cố định", tôi không cảm thấy chi phí từ điển là đáng giá.
Rostov

6
@Kody Tại sao bạn nói không sử dụng dispose? Tôi luôn vứt bỏ trừ khi tôi có lý do chính đáng để không sử dụng HttpClient.
Dan Friedman

41

TL; DR: không sử dụng phiên bản được chấp nhận vì nó hoàn toàn bị hỏng liên quan đến việc xử lý các ký tự unicode và không bao giờ sử dụng API nội bộ

Tôi thực sự đã tìm thấy vấn đề mã hóa kép kỳ lạ với giải pháp được chấp nhận:

Vì vậy, nếu bạn đang xử lý các ký tự cần được mã hóa, giải pháp được chấp nhận sẽ dẫn đến mã hóa kép:

  • tham số truy vấn được tự động mã hóa bằng cách sử dụng bộ NameValueCollectionchỉ mục ( và điều này sử dụng UrlEncodeUnicode, không được mong đợi thường xuyên UrlEncode(!) )
  • Sau đó, khi bạn gọi uriBuilder.Urinó sẽ tạo mới Uribằng cách sử dụng hàm tạo , mã hóa thêm một lần nữa (mã hóa url thông thường)
  • Điều đó không thể tránh được bằng cách thực hiệnuriBuilder.ToString() (mặc dù điều này trả về chính xác UriIMO ít nhất là không nhất quán, có thể là một lỗi, nhưng đó là một câu hỏi khác) và sau đó sử dụng HttpClientchuỗi chấp nhận phương thức - client vẫn tạo Urira chuỗi đã truyền của bạn như thế này:new Uri(uri, UriKind.RelativeOrAbsolute)

Nhỏ, nhưng repro đầy đủ:

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!

Đầu ra:

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

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

Như bạn có thể thấy, bất kể bạn làm uribuilder.ToString()+ httpClient.GetStringAsync(string)hay uriBuilder.Uri+ httpClient.GetStringAsync(Uri)cuối cùng bạn sẽ gửi tham số được mã hóa kép

Ví dụ cố định có thể là:

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

Nhưng điều này sử dụng lỗi thời Uri constructor

PS trên .NET mới nhất của tôi trên Windows Server, nhà Urixây dựng với nhận xét bool doc nói "lỗi thời, dontEscape luôn sai", nhưng thực sự hoạt động như mong đợi (bỏ qua thoát)

Vì vậy, nó trông giống như một lỗi khác ...

Và thậm chí điều này hoàn toàn sai - nó gửi UrlEncodingUnicode đến máy chủ, không chỉ UrlEncoding những gì máy chủ mong đợi

Cập nhật: một điều nữa là, NameValueCollection thực sự có UrlEncodeUnicode, không được sử dụng nữa và không tương thích với url.encode / giải mã thông thường (xem NameValueCollection để truy vấn URL? ).

Vì vậy, điểm mấu chốt là: không bao giờ sử dụng bản hack nàyNameValueCollection query = HttpUtility.ParseQueryString(builder.Query); vì nó sẽ làm rối các tham số truy vấn unicode của bạn. Chỉ cần xây dựng truy vấn theo cách thủ công và gán nó vào UriBuilder.Queryđó sẽ thực hiện mã hóa cần thiết và sau đó nhận Uri bằng cách sử dụng UriBuilder.Uri.

Ví dụ điển hình về việc tự làm tổn thương chính mình bằng cách sử dụng mã không được sử dụng như thế này


16
Bạn có thể thêm một chức năng tiện ích đầy đủ cho câu trả lời này hoạt động?
mafu 11/03/2016

8
Tôi thứ hai mafu về điều này: Tôi đọc qua câu trả lời nhưng không có kết luận. Có một câu trả lời dứt khoát cho điều này?
Richard Griffiths

3
Tôi cũng muốn xem câu trả lời dứt khoát cho vấn đề này
Pones

Câu trả lời dứt khoát cho vấn đề này là sử dụng var namedValues = HttpUtility.ParseQueryString(builder.Query), nhưng sau đó thay vì sử dụng NameValueCollection được trả về, ngay lập tức chuyển đổi nó thành Từ điển như sau: var dic = values.ToDictionary(x => x, x => values[x]); Thêm giá trị mới vào từ điển, sau đó chuyển nó đến hàm tạo của nó FormUrlEncodedContentvà gọi ReadAsStringAsync().Resultnó. Điều đó cung cấp cho bạn một chuỗi truy vấn được mã hóa chính xác, mà bạn có thể gán lại cho UriBuilder.
Triynko

Bạn thực sự chỉ có thể sử dụng NamedValueCollection.ToString thay vì tất cả những thứ đó, nhưng chỉ khi bạn thay đổi cài đặt app.config / web.config ngăn ASP.NET sử dụng mã hóa '% uXXXX' : <add key="aspnet:DontUsePercentUUrlEncoding" value="true" />. Tôi sẽ không phụ thuộc vào hành vi này, vì vậy tốt hơn là sử dụng lớp FormUrlEncodingContent, như đã được chứng minh bằng một câu trả lời trước đó: stackoverflow.com/a/26744471/88409
Triynko

41

Trong dự án ASP.NET Core, bạn có thể sử dụng lớp 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
Thật khó chịu khi mặc dù với quy trình này, bạn vẫn không thể gửi nhiều giá trị cho cùng một khóa. Nếu bạn muốn gửi "bar" và "bar2" như một phần của foo, điều đó là không thể.
m0g

2
Đây là một câu trả lời tuyệt vời cho các ứng dụng hiện đại, hoạt động theo kịch bản của tôi, đơn giản và rõ ràng. Tuy nhiên, tôi không cần bất kỳ cơ chế thoát nào - không được thử nghiệm.
Patrick Stalph

Gói NuGet này nhắm mục tiêu .NET chuẩn 2.0, có nghĩa là bạn có thể sử dụng nó trên toàn bộ .NET framework 4.6.1+
eddiewould

24

Bạn có thể muốn kiểm tra Flurl [tiết lộ: Tôi là tác giả], một người xây dựng URL thông thạo với lib đồng hành tùy chọn mở rộng nó thành một ứng dụng REST đầy đủ.

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

Kiểm tra các tài liệu để biết thêm chi tiết. Gói đầy đủ có sẵn trên NuGet:

PM> Install-Package Flurl.Http

hoặc chỉ là trình tạo URL độc lập:

PM> Install-Package Flurl


2
Tại sao không mở rộng Urihoặc bắt đầu với lớp học của riêng bạn thay vì string?
mở

2
Về mặt kỹ thuật tôi đã bắt đầu với Urllớp học của riêng tôi . Ở trên tương đương với new Url("https://api.com").AppendPathSegment...Cá nhân tôi thích các phần mở rộng chuỗi do ít thao tác bàn phím hơn và được chuẩn hóa trên chúng trong các tài liệu, nhưng bạn có thể làm theo cách đó.
Todd Menier

Lạc đề, nhưng lib thực sự tốt, tôi đang sử dụng nó sau khi thấy điều này. Cảm ơn bạn đã sử dụng IHttpClientFactory.
Ed S.

4

Dọc theo dòng giống như bài Rostov, nếu bạn không muốn bao gồm một tham chiếu đến System.Webtrong dự án của bạn, bạn có thể sử dụng FormDataCollectiontừ System.Net.Http.Formattingvà làm điều gì đó như sau:

Sử dụng 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 đưa ra một giải pháp thú vị và thông minh, và đây là một điều có thể là một lựa chọn khác:

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

và vì vậy khi sử dụng nó, bạn có thể làm điều này:

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

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

5
Bạn sẽ muốn mã hóa kvp.Keykvp.Valueriêng biệt bên trong vòng lặp for, không phải trong chuỗi truy vấn đầy đủ (do đó không mã hóa &=ký tự).
Matthew

Cảm ơn Mike. Các giải pháp được đề xuất khác (liên quan đến NameValueCollection) không hiệu quả với tôi vì tôi đang ở trong một dự án PCL, vì vậy đây là một giải pháp thay thế hoàn hảo. Đối với những người khác đang làm việc ở phía khách hàng, server.UrlEncodecó thể thay thế bằngWebUtility.UrlEncode
BCA

2

Hoặc đơn giản là sử dụng tiện ích mở rộng Uri của tôi

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

Sử dụng

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

Kết quả

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


27
Bạn không quên mã hóa URL?
Kugel

1
đây là một ví dụ tuyệt vời về việc sử dụng các tiện ích mở rộng để tạo các trợ giúp rõ ràng, hữu ích. Nếu bạn kết hợp câu trả lời này với câu trả lời được chấp nhận, bạn đang trên đường xây dựng một RestClient vững chắc
emran

2

Các RFC 6570 URI Template thư viện Tôi đang phát triển có khả năng thực hiện hoạt động này. Tất cả mã hóa được xử lý cho bạn theo RFC đó. Tại thời điểm viết bài này, bản phát hành beta đã có sẵn và lý do duy nhất nó không được coi là bản phát hành 1.0 ổn định là tài liệu không đáp ứng đầy đủ mong đợi của tôi (xem các vấn đề # 17 , # 18 , # 32 , # 43 ).

Bạn có thể xây dựng một chuỗi truy vấn một mình:

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

Hoặc bạn có thể xây dựng một URI hoàn chỉnh:

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

Vì tôi phải sử dụng lại vài lần, tôi đã nghĩ ra lớp này chỉ đơn giản là giúp trừu tượng hóa cách chuỗi truy vấn được tạo.

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

}

Việc sử dụng sẽ được đơn giản hóa thành một cái gì đó như thế này:

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

điều đó sẽ trả về uri: http://example.com/?foo=bar%3c%3e%26-baz&bar=second


1

Để tránh vấn đề mã hóa kép được mô tả trong câu trả lời của taras.roshko và để giữ khả năng dễ dàng làm việc với các tham số truy vấn, bạn có thể sử dụng uriBuilder.Uri.ParseQueryString()thay vì HttpUtility.ParseQueryString().


1

Một phần tốt của câu trả lời được chấp nhận, được sửa đổi để sử dụng UriBuilder.Uri.PudeQueryString () thay vì httpUtility.PudeQueryString ():

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

FYI: Điều này đòi hỏi phải tham chiếu đến System.Net.HttpParseQueryString()phương thức mở rộng không nằm trong System.
Nắng Patel

0

Nhờ "Darin Dimitrov", Đây là phương thức mở rộng.

 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

Tôi không thể tìm thấy giải pháp nào tốt hơn là tạo phương thức mở rộng để chuyển đổi Từ điển sang QueryStringFormat. Giải pháp được đề xuất bởi Waleed AK là tốt.

Thực hiện theo giải pháp của tôi:

Tạo phương thức mở rộng:

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

Và họ:

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
Giải pháp này thiếu mã hóa tham số URL thích hợp và sẽ không hoạt động với các giá trị chứa các ký tự 'không hợp lệ'
Xavier Poinas

Vui lòng cập nhật câu trả lời và thêm dòng mã hóa bị thiếu, đó chỉ là một dòng mã!
Diego Mendes
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.