Побудувати рядок запиту для отримання 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.