Как построить строку запроса для URL в C #? - PullRequest
441 голосов
/ 06 мая 2009

Обычной задачей при вызове веб-ресурсов из кода является построение строки запроса, включающей все необходимые параметры. Несмотря на то, что ни в коем случае не ракетостроение, есть некоторые изящные детали, о которых вам нужно позаботиться, например, добавление &, если не первый параметр, кодирование параметров и т. Д.

Код для этого очень прост, но немного утомителен:

StringBuilder SB = new StringBuilder();
if (NeedsToAddParameter A) 
{ 
  SB.Append("A="); SB.Append(HttpUtility.UrlEncode("TheValueOfA")); 
}

if (NeedsToAddParameter B) 
{
  if (SB.Length>0) SB.Append("&"); 
  SB.Append("B="); SB.Append(HttpUtility.UrlEncode("TheValueOfB")); }
}

Это такая распространенная задача, что можно ожидать существования служебного класса, который делает его более элегантным и читаемым. Сканируя MSDN, я не смог найти ни одного - который приводит меня к следующему вопросу:

Какой самый элегантный и чистый способ из перечисленных выше?

Ответы [ 34 ]

642 голосов
/ 10 декабря 2009

Вы можете создать новый доступный для записи экземпляр HttpValueCollection, вызвав System.Web.HttpUtility.ParseQueryString(string.Empty), а затем использовать его как любой NameValueCollection. После того, как вы добавили нужные значения, вы можете вызвать ToString в коллекции, чтобы получить строку запроса, следующим образом:

NameValueCollection queryString = System.Web.HttpUtility.ParseQueryString(string.Empty);

queryString["key1"] = "value1";
queryString["key2"] = "value2";

return queryString.ToString(); // Returns "key1=value1&key2=value2", all URL-encoded

HttpValueCollection является внутренним, и поэтому вы не можете напрямую создать экземпляр. Однако, как только вы получите экземпляр, вы можете использовать его, как и любой другой NameValueCollection. Поскольку фактический объект, с которым вы работаете, это HttpValueCollection, вызов метода ToString вызовет переопределенный метод для HttpValueCollection, который форматирует коллекцию как строку запроса в кодировке URL.

После поиска SO и в Интернете ответа на похожую проблему, это самое простое решение, которое я мог найти.

.NET Core

Если вы работаете в .NET Core, вы можете использовать класс Microsoft.AspNetCore.WebUtilities.QueryHelpers, который значительно упрощает это.

https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.webutilities.queryhelpers

279 голосов
/ 06 мая 2009

Если вы загляните внутрь, свойство QueryString будет NameValueCollection. Когда я делал подобные вещи, я обычно интересовался сериализацией и десериализацией, поэтому я предлагаю создать коллекцию NameValueCollection и затем перейти к:

using System.Web;
using System.Collections.Specialized;

private string ToQueryString(NameValueCollection nvc)
{
    var array = (from key in nvc.AllKeys
        from value in nvc.GetValues(key)
        select string.Format("{0}={1}", HttpUtility.UrlEncode(key), HttpUtility.UrlEncode(value)))
        .ToArray();
    return "?" + string.Join("&", array);
}

Возможно, я мог бы отформатировать это лучше:)

Я думаю, что в LINQ тоже есть супер элегантный способ сделать это ...

87 голосов
/ 31 мая 2012

Вдохновленный комментарием Роя Тинкера, я использовал простой метод расширения класса Uri, который делает мой код лаконичным и чистым:

using System.Web;

public static class HttpExtensions
{
    public static Uri AddQuery(this Uri uri, string name, string value)
    {
        var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);

        httpValueCollection.Remove(name);
        httpValueCollection.Add(name, value);

        var ub = new UriBuilder(uri);
        ub.Query = httpValueCollection.ToString();

        return ub.Uri;
    }
}

Использование:

Uri url = new Uri("http://localhost/rest/something/browse").
          AddQuery("page", "0").
          AddQuery("pageSize", "200");

Редактировать - вариант, соответствующий стандартам

Как отмечали несколько человек, httpValueCollection.ToString() кодирует символы Unicode нестандартным способом. Это вариант того же метода расширения, который обрабатывает такие символы, вызывая метод HttpUtility.UrlEncode вместо устаревшего метода HttpUtility.UrlEncodeUnicode.

using System.Web;

public static Uri AddQuery(this Uri uri, string name, string value)
{
    var httpValueCollection = HttpUtility.ParseQueryString(uri.Query);

    httpValueCollection.Remove(name);
    httpValueCollection.Add(name, value);

    var ub = new UriBuilder(uri);

    // this code block is taken from httpValueCollection.ToString() method
    // and modified so it encodes strings with HttpUtility.UrlEncode
    if (httpValueCollection.Count == 0)
        ub.Query = String.Empty;
    else
    {
        var sb = new StringBuilder();

        for (int i = 0; i < httpValueCollection.Count; i++)
        {
            string text = httpValueCollection.GetKey(i);
            {
                text = HttpUtility.UrlEncode(text);

                string val = (text != null) ? (text + "=") : string.Empty;
                string[] vals = httpValueCollection.GetValues(i);

                if (sb.Length > 0)
                    sb.Append('&');

                if (vals == null || vals.Length == 0)
                    sb.Append(val);
                else
                {
                    if (vals.Length == 1)
                    {
                        sb.Append(val);
                        sb.Append(HttpUtility.UrlEncode(vals[0]));
                    }
                    else
                    {
                        for (int j = 0; j < vals.Length; j++)
                        {
                            if (j > 0)
                                sb.Append('&');

                            sb.Append(val);
                            sb.Append(HttpUtility.UrlEncode(vals[j]));
                        }
                    }
                }
            }
        }

        ub.Query = sb.ToString();
    }

    return ub.Uri;
}
30 голосов
/ 06 мая 2009

Я ответил на похожий вопрос некоторое время назад. По сути, лучшим способом было бы использовать класс HttpValueCollection, которым на самом деле является свойство Request.QueryString ASP.NET, к сожалению, оно является внутренним в фреймворке .NET. Вы можете использовать Reflector, чтобы захватить его (и поместить в свой класс Utils). Таким образом, вы можете манипулировать строкой запроса, как NameValueCollection, но с учетом всех проблем кодирования / декодирования URL-адреса.

HttpValueCollection расширяет NameValueCollection и имеет конструктор, который принимает строку запроса в кодировке (включая амперсанды и вопросительные знаки) и переопределяет метод ToString() для последующего перестроения строки запроса из базовая коллекция.

Пример:

  var coll = new HttpValueCollection();

  coll["userId"] = "50";
  coll["paramA"] = "A";
  coll["paramB"] = "B";      

  string query = coll.ToString(true); // true means use urlencode

  Console.WriteLine(query); // prints: userId=50&paramA=A&paramB=B
28 голосов
/ 04 января 2012

Вот свободный / лямбда-способ в качестве метода расширения (объединение понятий в предыдущих статьях), который поддерживает несколько значений для одного и того же ключа. Мое личное предпочтение - расширение над оболочками для возможности обнаружения другими членами команды для подобных вещей. Обратите внимание, что существуют разногласия по поводу методов кодирования, о нем много сообщений о переполнении стека (один такой post ) и блоггерах MSDN (например, этот ).

public static string ToQueryString(this NameValueCollection source)
{
    return String.Join("&", source.AllKeys
        .SelectMany(key => source.GetValues(key)
            .Select(value => String.Format("{0}={1}", HttpUtility.UrlEncode(key), HttpUtility.UrlEncode(value))))
        .ToArray());
}

edit: с нулевой поддержкой, хотя вам, вероятно, придется адаптировать ее для вашей конкретной ситуации

public static string ToQueryString(this NameValueCollection source, bool removeEmptyEntries)
{
    return source != null ? String.Join("&", source.AllKeys
        .Where(key => !removeEmptyEntries || source.GetValues(key)
            .Where(value => !String.IsNullOrEmpty(value))
            .Any())
        .SelectMany(key => source.GetValues(key)
            .Where(value => !removeEmptyEntries || !String.IsNullOrEmpty(value))
            .Select(value => String.Format("{0}={1}", HttpUtility.UrlEncode(key), value != null ? HttpUtility.UrlEncode(value) : string.Empty)))
        .ToArray())
        : string.Empty;
}
21 голосов
/ 21 февраля 2014

Flurl [раскрытие: я автор] поддерживает создание строк запроса с помощью анонимных объектов (среди прочих способов):

var url = "http://www.some-api.com".SetQueryParams(new
{
    api_key = ConfigurationManager.AppSettings["SomeApiKey"],
    max_results = 20,
    q = "Don't worry, I'll get encoded!"
});

Необязательный сопутствующий lib Flurl.Http позволяет выполнять HTTP-вызовы сразу из одной и той же цепочки вызовов, расширяя ее до полноценного REST-клиента:

T result = await "https://api.mysite.com"
    .AppendPathSegment("person")
    .SetQueryParams(new { ap_key = "my-key" })
    .WithOAuthBearerToken("MyToken")
    .PostJsonAsync(new { first_name = firstName, last_name = lastName })
    .ReceiveJson<T>();

Полный пакет доступен на NuGet:

PM> Install-Package Flurl.Http

или просто автономный построитель URL:

PM> Install-Package Flurl

19 голосов
/ 02 августа 2011

Вот моя поздняя запись. Я не любил других по разным причинам, поэтому написал свою.

Особенности этой версии:

  • Использование только StringBuilder. Нет вызовов ToArray () или других методов расширения. Он выглядит не так красиво, как некоторые другие ответы, но я считаю, что это основная функция, поэтому эффективность важнее, чем «свободный», «однострочный» код, скрывающий неэффективность.

  • Обрабатывает несколько значений на клавишу. (Сам не нуждался в этом, только чтобы заставить замолчать Маурисио;)

    public string ToQueryString(NameValueCollection nvc)
    {
        StringBuilder sb = new StringBuilder("?");
    
        bool first = true;
    
        foreach (string key in nvc.AllKeys)
        {
            foreach (string value in nvc.GetValues(key))
            {
                if (!first)
                {
                    sb.Append("&");
                }
    
                sb.AppendFormat("{0}={1}", Uri.EscapeDataString(key), Uri.EscapeDataString(value));
    
                first = false;
            }
        }
    
        return sb.ToString();
    }
    

Пример использования

        var queryParams = new NameValueCollection()
        {
            { "x", "1" },
            { "y", "2" },
            { "foo", "bar" },
            { "foo", "baz" },
            { "special chars", "? = &" },
        };

        string url = "http://example.com/stuff" + ToQueryString(queryParams);

        Console.WriteLine(url);

выход

http://example.com/stuff?x=1&y=2&foo=bar&foo=baz&special%20chars=%3F%20%3D%20%26
11 голосов
/ 06 мая 2009

Как насчет создания методов расширения, которые позволяют вам добавлять параметры в свободном стиле, как это?

string a = "http://www.somedomain.com/somepage.html"
    .AddQueryParam("A", "TheValueOfA")
    .AddQueryParam("B", "TheValueOfB")
    .AddQueryParam("Z", "TheValueOfZ");

string b = new StringBuilder("http://www.somedomain.com/anotherpage.html")
    .AddQueryParam("A", "TheValueOfA")
    .AddQueryParam("B", "TheValueOfB")
    .AddQueryParam("Z", "TheValueOfZ")
    .ToString(); 

Вот перегрузка, которая использует string:

public static string AddQueryParam(
    this string source, string key, string value)
{
    string delim;
    if ((source == null) || !source.Contains("?"))
    {
        delim = "?";
    }
    else if (source.EndsWith("?") || source.EndsWith("&"))
    {
        delim = string.Empty;
    }
    else
    {
        delim = "&";
    }

    return source + delim + HttpUtility.UrlEncode(key)
        + "=" + HttpUtility.UrlEncode(value);
}

А вот перегрузка, которая использует StringBuilder:

public static StringBuilder AddQueryParam(
    this StringBuilder source, string key, string value)
{
    bool hasQuery = false;
    for (int i = 0; i < source.Length; i++)
    {
        if (source[i] == '?')
        {
            hasQuery = true;
            break;
        }
    }

    string delim;
    if (!hasQuery)
    {
        delim = "?";
    }
    else if ((source[source.Length - 1] == '?')
        || (source[source.Length - 1] == '&'))
    {
        delim = string.Empty;
    }
    else
    {
        delim = "&";
    }

    return source.Append(delim).Append(HttpUtility.UrlEncode(key))
        .Append("=").Append(HttpUtility.UrlEncode(value));
}
10 голосов
/ 31 июля 2014

Мне нужно было решить ту же проблему для переносимой библиотеки классов (PCL), над которой я работаю. В этом случае у меня нет доступа к System.Web, поэтому я не могу использовать ParseQueryString.

Вместо этого я использовал System.Net.Http.FormUrlEncodedContent вот так:

var url = new UriBuilder("http://example.com");

url.Query = new FormUrlEncodedContent(new Dictionary<string,string>()
{
    {"param1", "val1"},
    {"param2", "val2"},
    {"param3", "val3"},
}).ReadAsStringAsync().Result;
9 голосов
/ 01 марта 2011
    public static string ToQueryString(this Dictionary<string, string> source)
    {
        return String.Join("&", source.Select(kvp => String.Format("{0}={1}", HttpUtility.UrlEncode(kvp.Key), HttpUtility.UrlEncode(kvp.Value))).ToArray());
    }

    public static string ToQueryString(this NameValueCollection source)
    {
        return String.Join("&", source.Cast<string>().Select(key => String.Format("{0}={1}", HttpUtility.UrlEncode(key), HttpUtility.UrlEncode(source[key]))).ToArray());
    }
...