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

184

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

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

NeoDarque
источник
@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();
cwills
источник
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

Jaider
источник
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();
Jpsy
источник
К вашему сведению: для этого требуется ссылка на 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-кодировка параметров, и он не будет работать со значениями, содержащими «недопустимые» символы
Ксавье Поинас
Не стесняйтесь обновить ответ и добавить отсутствующую строку кодировки, это просто строка кода!
Диего Мендес