Строка запроса сборки для System.Net.HttpClient get


184

Если я хочу отправить запрос http get с помощью System.Net.HttpClient, кажется, что нет api для добавления параметров, это правильно?

Есть ли простой API, доступный для создания строки запроса, которая не включает создание коллекции значений имени и URL, кодирующих их, а затем, наконец, объединяет их? Я надеялся использовать что-то вроде API RestSharp (т.е. AddParameter (..))


@Michael Perrenoud Вы можете пересмотреть, используя принятый ответ с символами, которые необходимо кодировать, см. Мое объяснение ниже
— нелегальный иммигрант

Ответы:


309

Если я хочу отправить запрос http get с помощью 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;
}

Почему вы используете оператор использования?
— Ян

Скорее всего, освободить ресурсы, но это слишком. Не делай этого.
— Коди

5
Это может быть более кратким, используя Dictionary <string, string> вместо массива KVP. Затем с помощью синтаксиса инициализатора: {"ham", "Glazed?" }
— Шон Б

@SeanB Это хорошая идея, особенно когда используется что-то для добавления динамического / неизвестного списка параметров. Для этого примера, поскольку это «фиксированный» список, я не чувствовал, что затраты на словарь того стоили.
— Ростов

6
@ Коди Почему вы говорите, чтобы не использовать dispose? Я всегда выбрасывайте , если у меня есть веские причины не делать этого , как повторное использование HttpClient.
— Дэн Фридман

41

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

На самом деле я обнаружил странную проблему двойного кодирования с принятым решением:

Итак, если вы имеете дело с символами, которые необходимо кодировать, то принятое решение приводит к двойному кодированию:

  • параметры запроса автоматически кодируются с помощью NameValueCollectionиндексатора ( и это использует UrlEncodeUnicode, а не обычное ожидание UrlEncode(!) )
  • Затем при вызове uriBuilder.Uriон создает новый Uriконструктор using, который выполняет кодирование еще раз (обычное кодирование URL).
  • Этого нельзя избежать, выполнивuriBuilder.ToString() (даже если это вернёт правильно, Uriчто IMO - это как минимум несоответствие, может быть, ошибка, но это другой вопрос), а затем с помощью 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 Query? ).

Итак, суть в том, что никогда не используйте этот хак,NameValueCollection query = HttpUtility.ParseQueryString(builder.Query); поскольку он испортит параметры вашего запроса в Юникоде. Просто создайте запрос вручную и назначьте его, 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 Framework 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в свой проект, вы можете использовать FormDataCollectionfrom 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отдельно внутри цикла for, а не в полной строке запроса (таким образом, не кодируя &, а =символы).
— Мэтью

Спасибо майк Другие предложенные решения (включая 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();

К вашему сведению: для этого требуется ссылка на 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-кодировка параметров, и он не будет работать со значениями, содержащими «недопустимые» символы
— Ксавье Поинас

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