Побудувати рядок запиту для отримання System.Net.HttpClient


184

Якщо я хочу подати запит на отримання http за допомогою System.Net.HttpClient, схоже, немає api для додавання параметрів, чи правильно це?

Чи є якісь прості api для створення рядка запиту, який не передбачає побудови колекції значень імен та URL-кодування, а потім остаточно об'єднати їх? Я сподівався використати щось на зразок api RestSharp (тобто AddParameter (..))


@Michael Perrenoud, можливо, ви захочете переглянути, використовуючи прийняту відповідь із символами, які потребують кодування, дивіться моє пояснення нижче
нелегальний іммігрант

Відповіді:


309

Якщо я хочу подати запит на отримання http за допомогою System.Net.HttpClient, схоже, немає api для додавання параметрів, чи правильно це?

Так.

Чи є якісь прості api для створення рядка запиту, який не передбачає побудови колекції значень імен та URL-кодування, а потім остаточно об'єднати їх?

Звичайно:

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

дасть очікуваний результат:

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

UriBuilderКлас також може бути корисним:

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

дасть очікуваний результат:

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

що ви могли б більш ніж безпечно подати свій HttpClient.GetAsyncметод.


9
Це абсолютно найкраще з точки зору обробки URL-адрес у .NET. Ніколи не потрібно кодувати URL-адреси вручну та робити об'єднання рядків чи строковиків чи будь-що інше. Клас UriBuilder навіть обробляє URL-адреси з фрагментами ( #), використовуючи властивість Fragment. Я бачив так багато людей, які роблять помилку вручну обробляти URL-адреси, а не використовувати вбудовані інструменти.
Дарин Димитров

6
NameValueCollection.ToString()як правило, не створює рядки запитів, і немає документації, в якій зазначається, що виконання ToStringрезультату в ParseQueryStringрезультаті призведе до появи нової рядка запиту, таким чином може бути зламана в будь-який час, оскільки в цій функціональності немає гарантії.
Метью

11
HttpUtility є у System.Web, який недоступний для портативного виконання. Дивно здається, що ця функціональність не є більш загальною в бібліотеках класів.
Кріс Елдредж

82
Цей розчин викликає негативний характер. .Net повинен мати належну програму побудови запитів.
Кугель

8
Те, що найкраще рішення приховане у внутрішньому класі, до якого ви можете отримати лише виклик утилітного методу, що проходить у порожній рядку, не можна точно назвати елегантним рішенням.
Кугель

79

Для тих , хто не хоче включати System.Webв проекти , які вже не використовуються, ви можете використовувати FormUrlEncodedContentз System.Net.Httpі зробити що - щось подібне до наступного:

версія ключових параметрів

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

версія словника

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

Чому ви використовуєте оператор use?
Ян Варбуртон

Ймовірно, щоб звільнити ресурси, але це надмірно. Не робіть цього.
Коді

5
Це може бути більш стислим, використовуючи словник <string, string> замість масиву KVP. Потім використовуючи синтаксис ініціалізатора: {"ham", "Glazed?" }
Шон Б

@SeanB Це гарна ідея, особливо коли використовується щось для додавання динамічного / невідомого списку параметрів. Для цього прикладу, оскільки це "фіксований" список, я не відчував, що накладні дані словника того варті.
Ростов

6
@Kody Чому ви скажете не використовувати dispose? Я завжди розпоряджаюся, якщо не маю поважних причин цього не робити, як повторне використання HttpClient.
Ден Фрідман

41

TL; DR: не використовуйте прийняту версію, оскільки вона повністю зламана стосовно обробки символів Unicode, і ніколи не використовуйте внутрішній API

Я фактично знайшов дивну проблему подвійного кодування з прийнятим рішенням:

Отже, якщо ви маєте справу з символами, які потрібно закодувати, прийняте рішення призводить до подвійного кодування:

  • Параметри запиту кодуються автоматично за допомогою NameValueCollectionіндексатора ( і це використовує UrlEncodeUnicode, не регулярно очікуване UrlEncode(!) )
  • Потім, коли ви телефонуєте, uriBuilder.Uriвін створює новий Uriза допомогою конструктора, який виконує кодування ще раз (звичайне кодування URL)
  • Цього не можна уникнути,uriBuilder.ToString() незважаючи на те, що якщо ви повертаєте правильно, UriIMO має принаймні непослідовність, можливо, помилку, але це вже інше питання), а потім використовуючи HttpClientметод прийняття рядка - клієнт все одно створює Uriз вашої переданої рядки так:new Uri(uri, UriKind.RelativeOrAbsolute)

Невелике, але повне докори:

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!

Вихід:

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

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

Як ви бачите, незалежно від того, зробите ви uribuilder.ToString()+ httpClient.GetStringAsync(string)або uriBuilder.Uri+, httpClient.GetStringAsync(Uri)ви в кінцевому підсумку надсилаєте подвійний закодований параметр

Фіксованим прикладом може бути:

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

Але для цього використовується застарілий Uri конструктор

PS в моєму останньому .NET на Windows Server, Uriконструктор з коментарем bool doc говорить: "застарілий, dontEscape завжди помилковий", але насправді працює як очікувалося (пропускає втечу)

Так виглядає ще одна помилка ...

І навіть це явно неправильно - він надсилає UrlEncodedUnicode на сервер, а не просто UrlEncoded, що сервер очікує

Оновлення: ще одне - NameValueCollection насправді робить UrlEncodeUnicode, який більше не повинен використовуватися і несумісний із звичайним url.encode / decode (див. NameValueCollection до URL-запиту? ).

Отже, підсумок: ніколи не використовуйте цей злом,NameValueCollection query = HttpUtility.ParseQueryString(builder.Query); оскільки він зіпсує ваші параметри запиту unicode. Просто створіть запит вручну та призначте його, UriBuilder.Queryякий зробить необхідне кодування, а потім отримайте Uri з використанням UriBuilder.Uri.

Прекрасний приклад пошкодження себе, використовуючи код, який не повинен використовуватися таким чином


16
Чи можете ви додати повну функціональну функцію до цієї відповіді, яка працює?
мафу

8
Я другий мафу щодо цього: я прочитав відповідь, але не маю висновку. Чи є на це остаточна відповідь?
Річард Гріффітс

3
Я також хотів би побачити остаточну відповідь на цю проблему
Pones

Остаточною відповіддю на цю проблему є використання var namedValues = HttpUtility.ParseQueryString(builder.Query), але потім замість використання повернутого NameValueCollection негайно перетворіть його у словник так: var dic = values.ToDictionary(x => x, x => values[x]); Додайте нові значення до словника, потім передайте його конструктору FormUrlEncodedContentта викликайте ReadAsStringAsync().Resultйого. Це дає вам правильно закодовану рядок запиту, яку ви можете призначити назад UriBuilder.
Трайнко

На насправді ви можете просто використовувати NamedValueCollection.ToString замість всього цього, але тільки якщо ви змінити app.config / web.config параметр , який запобігає ASP.NET за допомогою «% іхххх» кодування: <add key="aspnet:DontUsePercentUUrlEncoding" value="true" />. Я б не залежати від цього поведінки, так що краще використовувати FormUrlEncodedContent клас, як показано на більш ранній відповідь: stackoverflow.com/a/26744471/88409
Triynko

41

У проекті ASP.NET Core ви можете використовувати клас 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
Прикро, що хоч за допомогою цього процесу ви все одно не можете надсилати кілька значень для одного ключа. Якщо ви хочете надіслати "bar" та "bar2" як частину foo, це неможливо.
m0g

2
Це чудова відповідь для сучасних додатків, працює за моїм сценарієм, простий і чистий. Однак мені не потрібні механізми втечі - не перевірені.
Патрік Стальф

Цей пакет NuGet орієнтований на .NET стандарт 2.0, що означає, що ви можете використовувати його в повному обсязі .NET 4.6.1+
eddiewould

24

Ви можете перевірити Flurl [розкриття: Я автор], вільний конструктор URL-адрес з додатковою супутниковою вкладкою, який поширює його на повноцінного клієнта 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>();

Ознайомтеся з документами для отримання більш детальної інформації. Повний пакет доступний на NuGet:

PM> Install-Package Flurl.Http

або просто окремий конструктор URL-адрес:

PM> Install-Package Flurl


2
Чому б не продовжити Uriчи не почати з власного класу замість string?
mpen

2
Технічно я почав із власного Urlкласу. Наведене вище еквівалентно new Url("https://api.com").AppendPathSegment...Особисто я віддаю перевагу розширенням рядків через меншу кількість натискань клавіш і стандартизованих на них у документах, але ви можете це зробити будь-яким способом.
Тодд Меньє

Тема поза темою, але дуже приємна лайка, я використовую її, побачивши це. Дякуємо, що також використовуєте IHttpClientFactory.
Ред С.

4

У тому ж ключі , як пост Ростова, якщо ви не хочете , щоб включити посилання на System.Webу вашому проекті, ви можете використовувати FormDataCollectionз System.Net.Http.Formattingі зробити що - щось подібне до наступного:

Використання 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

Дарин запропонував цікаве і розумне рішення, і ось щось, що може бути іншим варіантом:

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

і тому при його використанні ви можете зробити це:

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

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

5
Ви хотіли б, щоб закодувати kvp.Keyі kvp.Valueокремо всередині для циклу, не в повній рядку запиту (таким чином , що не кодують &, і =символів).
Метью

Спасибі Майку. Інші запропоновані рішення (за участю NameValueCollection) не працювали для мене, оскільки я перебуваю в проекті PCL, тому це була ідеальна альтернатива. Для інших, хто працює з клієнтом, server.UrlEncodeможна замінити наWebUtility.UrlEncode
BCA

2

Або просто за допомогою мого розширення Uri

Код

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

Використання

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

Результат

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


27
Ви не забули кодування URL-адрес?
Кугель

1
це чудовий приклад використання розширень для створення чітких корисних помічників. Якщо ви поєднаєте це з прийнятою відповіддю, ви збираєтеся створити міцний RestClient
emran

2

RFC 6570 URI Бібліотека шаблонів Я розробляю здатний виконувати цю операцію. Все кодування обробляється для вас відповідно до цього RFC. На момент написання цього повідомлення доступна бета-версія, і єдина причина, що її не вважають стабільною версією 1.0, - це те, що документація не відповідає моїм очікуванням (див. Випуски №17 , №18 , №32 , №43 ).

Ви можете створити рядок запиту самостійно:

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

Або ви можете створити повний 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);

1

Оскільки мені доведеться повторно використовувати цей час, я придумав цей клас, який просто допомагає абстрагувати, як складається рядок запиту.

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

}

Використання буде спрощено до подібного:

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

що поверне uri: http://example.com/?foo=bar%3c%3e%26-baz&bar=second


1

Щоб уникнути проблеми подвійного кодування, описаної у відповіді taras.roshko, та зберегти можливість легко працювати з параметрами запиту, ви можете використовувати uriBuilder.Uri.ParseQueryString()замість HttpUtility.ParseQueryString().


1

Добра частина прийнятої відповіді, модифікованої для використання UriBuilder.Uri.ParseQueryString () замість 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();

FYI: для цього потрібна посилання на System.Net.Http, оскільки ParseQueryString()метод розширення не знаходиться в межах System.
Сонячний Патель

0

Завдяки "Дарин Димитров", Це методи розширення.

 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

Я не зміг знайти кращого рішення, ніж створити метод розширення для перетворення словника в QueryStringFormat. Також добре рішення, запропоноване Waleed AK.

Дотримуйтесь мого рішення:

Створіть метод розширення:

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

А вони:

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
У цьому рішенні відсутнє правильне кодування URL-адрес параметрів, і воно не працюватиме зі значеннями, що містять символи "Недійсні"
Xavier Poinas

Не соромтеся оновити відповідь та додати відсутність рядка кодування, це лише рядок коду!
Дієго Мендес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.