如何在C#中构建URL的查询字符串?

602

在代码中从网络资源获取数据时,常见的任务是构建查询字符串以包含所有必要的参数。虽然这并不是什么高深技术,但您需要注意一些巧妙的细节,例如:如果不是第一个参数,则附加&,对参数进行编码等。

执行此操作的代码非常简单,但有点繁琐:

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,我未能找到一个—这就引出了以下问题:

你知道最优雅且清晰的方法是什么吗?


32
目前看来,处理查询字符串似乎没有一种简单明了的方式,这有点令人沮丧。所谓简单明了,是指使用一个开箱即用、非内部实现、符合标准的框架类。或者我可能遗漏了某些东西? - Grimace of Despair
6
你没有错过什么。构建查询字符串是这个框架中的一个主要缺陷,我已经尝试用Flurl来填补这个空白。 - Todd Menier
2
我个人使用的技术是我在这个问题中引用的技术:https://dev59.com/YmQn5IYBdhLWcg3wGT8X#26744471 - Rostov
你让我想到了我应该构建一个.. new UrlBuilder(existing).AddQuery("key", "value").ToString() - Demetris Leptos
这个答案同样适用于嵌套对象 链接描述 - giorgi02
41个回答

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

1
不错!但是你不需要.ToArray() - mpen

9
将此类添加到您的项目中。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

public class QueryStringBuilder
{
    private readonly List<KeyValuePair<string, object>> _list;

    public QueryStringBuilder()
    {
        _list = new List<KeyValuePair<string, object>>();
    }

    public void Add(string name, object value)
    {
        _list.Add(new KeyValuePair<string, object>(name, value));
    }

    public override string ToString()
    {
        return String.Join("&", _list.Select(kvp => String.Concat(Uri.EscapeDataString(kvp.Key), "=", Uri.EscapeDataString(kvp.Value.ToString()))));
    }
}

然后像这样使用:

var actual = new QueryStringBuilder {
    {"foo", 123},
    {"bar", "val31"},
    {"bar", "val32"}
};

actual.Add("a+b", "c+d");

actual.ToString(); // "foo=123&bar=val31&bar=val32&a%2bb=c%2bd"

现在,这应该是被接受的答案;因为它完美地适用于像“foo[]=1,foo[]=2”这样的数组,并且保持参数顺序非常重要。 - Soroush Falahati

9

在dotnet core中,QueryHelpers.AddQueryString()会接受一个IDictionary<string,string>类型的键值对。为了节省一些内存分配和CPU开销,你可以使用SortedList<,>代替Dictionary<,>,并设置适当的容量,并按顺序添加项...

var queryParams = new SortedList<string,string>(2);
queryParams.Add("abc", "val1");
queryParams.Add("def", "val2");

string requestUri = QueryHelpers.AddQueryString("https://localhost/api", queryParams);

虽然这似乎是一个非常优雅的解决方案,但由于它不支持数组,所以存在问题。我尝试发送一个ID列表。https://github.com/aspnet/AspNetCore/issues/7945 - Sven

8
未经测试,但我认为这样的东西会非常好用。
public class QueryString
{
    private Dictionary<string,string> _Params = new Dictionary<string,string>();

    public overide ToString()
    {
        List<string> returnParams = new List<string>();

        foreach (KeyValuePair param in _Params)
        {
            returnParams.Add(String.Format("{0}={1}", param.Key, param.Value));
        }

        // return String.Format("?{0}", String.Join("&", returnParams.ToArray())); 

        // credit annakata
        return "?" + String.Join("&", returnParams.ToArray());
    }

    public void Add(string key, string value)
    {
        _Params.Add(key, HttpUtility.UrlEncode(value));
    }
}

QueryString query = new QueryString();

query.Add("param1", "value1");
query.Add("param2", "value2");

return query.ToString();

封装得很好,但是"?{0}"的格式有点不必要地昂贵 :) - annakata
将类名更改为 QueryString。Query 有点模糊不清。 - Nick Allen

7

我的服务:

public static Uri AddQuery(this Uri uri, string name, string value)
{
    // this actually returns HttpValueCollection : NameValueCollection
    // which uses unicode compliant encoding on ToString()
    var query = HttpUtility.ParseQueryString(uri.Query);

    query.Add(name, value);

    var uriBuilder = new UriBuilder(uri)
    {
        Query = query.ToString()
    };

    return uriBuilder.Uri;
}

使用方法:

var uri = new Uri("http://stackoverflow.com").AddQuery("such", "method")
                                             .AddQuery("wow", "soFluent");

// http://stackoverflow.com?such=method&wow=soFluent

我喜欢你的简洁优雅的做法,但是 HttpUtility 需要 System.Web。 - Ody

6

将顶部答案结合起来创建一个匿名对象版本

var queryString = HttpUtility2.BuildQueryString(new
{
    key2 = "value2",
    key1 = "value1",
});

这会生成如下内容:

key2=value2&key1=value1

以下是代码:

public static class HttpUtility2
{
    public static string BuildQueryString<T>(T obj)
    {
        var queryString = HttpUtility.ParseQueryString(string.Empty);

        foreach (var property in TypeDescriptor.GetProperties(typeof(T)).Cast<PropertyDescriptor>())
        {
            var value = (property.GetValue(obj) ?? "").ToString();
            queryString.Add(property.Name, value);
        }

        return queryString.ToString();
    }
}

使用动态对象和反射而不是其他选项,如NameValueCollection,似乎不是一个好主意。考虑到查询参数的名称是未知的(可变的),就像一些Web API一样。 - S.Serpooshan
使用动态对象和反射而不是其他选项,如NameValueCollection,似乎不是一个好主意。考虑到查询参数的名称是未知的(变量),就像一些Web API一样。 - undefined

6

这里有很多好的答案,但对于那些使用现代C#的人来说,这可能是一个不错的实用类。

public class QueryParamBuilder
{
    private readonly Dictionary<string, string> _fields = new();
    public QueryParamBuilder Add(string key, string value)
    {
        _fields.Add(key, value);
        return this;
    }
    public string Build()
    {
        return $"?{String.Join("&", _fields.Select(pair => $"{HttpUtility.UrlEncode(pair.Key)}={HttpUtility.UrlEncode(pair.Value)}"))}";
    }
    public static QueryParamBuilder New => new();
}

我在这里使用一个内部的 Dictionary,因为字典在内部是可枚举的键值对,这使得在它们上面迭代比NameValueCollection更容易。

然后查询字符串本身是一个简单的插值字符串加上一个连接符。

此外,我提供了一个静态接口到构造函数,以使构建新的生成器非常容易,并且只允许暴露一个方法 Add 来添加新的查询参数值。最后,你可以用 Build() 来终止链式调用,从而获得最终的字符串。

以下是其使用示例:

var queryString = QueryParamBuilder.New
     .Add("id", "0123")
     .Add("value2", 1234.ToString())
     .Add("valueWithSpace","value with spa12!@#@!ce")
     .Build();

结果和预期一样

?id=0123&value2=1234&valueWithSpace=value+with+spa12!%40%23%40!ce

希望这个能让你们感到舒适优雅。


1
有些服务器接受重复的键并将它们转换为重复键的值数组。在建议的实现中,您正在使用不允许重复键的字典。 - Rustem Zinnatullin

5

一个基于扩展方法的快速版本:

class Program
{
    static void Main(string[] args)
    {
        var parameters = new List<KeyValuePair<string, string>>
                             {
                                 new KeyValuePair<string, string>("A", "AValue"),
                                 new KeyValuePair<string, string>("B", "BValue")
                             };

        string output = "?" + string.Join("&", parameters.ConvertAll(param => param.ToQueryString()).ToArray());
    }
}

public static class KeyValueExtensions
{
    public static string ToQueryString(this KeyValuePair<string, string> obj)
    {
        return obj.Key + "=" + HttpUtility.UrlEncode(obj.Value);
    }
}

您可以使用 where 子句选择要添加到字符串中的参数。

4

HttpValueCollection的可链式包装类:

namespace System.Web.Mvc {
    public class QueryStringBuilder {
        private NameValueCollection collection;
        public QueryStringBuilder() {
            collection = System.Web.HttpUtility.ParseQueryString(string.Empty);
        }
        public QueryStringBuilder Add(string key, string value) {
            collection.Add(key, value);
            return this;
        }
        public QueryStringBuilder Remove(string key) {
            collection.Remove(key);
            return this;
        }
        public string this[string key] {
            get { return collection[key]; }
            set { collection[key] = value; }
        }
        public string ToString() {
            return collection.ToString();
        }
    }
}

使用示例:

QueryStringBuilder parameters = new QueryStringBuilder()
    .Add("view", ViewBag.PageView)
    .Add("page", ViewBag.PageNumber)
    .Add("size", ViewBag.PageSize);
string queryString = parameters.ToString();

3
我有一个针对Uri的扩展方法,它具有以下特点:
  • 接受匿名对象:uri.WithQuery(new { name = "value" })
  • 接受string/string键值对集合(例如Dictionary`2)。
  • 接受string/object键值对集合(例如RouteValueDictionary)。
  • 接受NameValueCollection
  • 按键排序查询值,以便相同的值产生相等的URI。
  • 支持多个值的每个键,保留它们的原始顺序。

文档版本可以在这里找到。

该扩展方法:

public static Uri WithQuery(this Uri uri, object values)
{
    if (uri == null)
        throw new ArgumentNullException(nameof(uri));

    if (values != null)
    {
        var query = string.Join(
            "&", from p in ParseQueryValues(values)
                 where !string.IsNullOrWhiteSpace(p.Key)
                 let k = HttpUtility.UrlEncode(p.Key.Trim())
                 let v = HttpUtility.UrlEncode(p.Value)
                 orderby k
                 select string.IsNullOrEmpty(v) ? k : $"{k}={v}");

        if (query.Length != 0 || uri.Query.Length != 0)
            uri = new UriBuilder(uri) { Query = query }.Uri;
    }

    return uri;
}

查询解析器:

private static IEnumerable<KeyValuePair<string, string>> ParseQueryValues(object values)
{
    // Check if a name/value collection.
    var nvc = values as NameValueCollection;
    if (nvc != null)
        return from key in nvc.AllKeys
               from val in nvc.GetValues(key)
               select new KeyValuePair<string, string>(key, val);

    // Check if a string/string dictionary.
    var ssd = values as IEnumerable<KeyValuePair<string, string>>;
    if (ssd != null)
        return ssd;

    // Check if a string/object dictionary.
    var sod = values as IEnumerable<KeyValuePair<string, object>>;
    if (sod == null)
    {
        // Check if a non-generic dictionary.
        var ngd = values as IDictionary;
        if (ngd != null)
            sod = ngd.Cast<dynamic>().ToDictionary<dynamic, string, object>(
                p => p.Key.ToString(), p => p.Value as object);

        // Convert object properties to dictionary.
        if (sod == null)
            sod = new RouteValueDictionary(values);
    }

    // Normalize and return the values.
    return from pair in sod
           from val in pair.Value as IEnumerable<string>
            ?? new[] { pair.Value?.ToString() }
           select new KeyValuePair<string, string>(pair.Key, val);
}

以下是测试内容:
var uri = new Uri("https://stackoverflow.com/yo?oldKey=oldValue");

// Test with a string/string dictionary.
var q = uri.WithQuery(new Dictionary<string, string>
{
    ["k1"] = string.Empty,
    ["k2"] = null,
    ["k3"] = "v3"
});

Debug.Assert(q == new Uri(
    "https://stackoverflow.com/yo?k1&k2&k3=v3"));

// Test with a string/object dictionary.
q = uri.WithQuery(new Dictionary<string, object>
{
    ["k1"] = "v1",
    ["k2"] = new[] { "v2a", "v2b" },
    ["k3"] = null
});

Debug.Assert(q == new Uri(
    "https://stackoverflow.com/yo?k1=v1&k2=v2a&k2=v2b&k3"));

// Test with a name/value collection.
var nvc = new NameValueCollection()
{
    ["k1"] = string.Empty,
    ["k2"] = "v2a"
};

nvc.Add("k2", "v2b");

q = uri.WithQuery(nvc);
Debug.Assert(q == new Uri(
    "https://stackoverflow.com/yo?k1&k2=v2a&k2=v2b"));

// Test with any dictionary.
q = uri.WithQuery(new Dictionary<int, HashSet<string>>
{
    [1] = new HashSet<string> { "v1" },
    [2] = new HashSet<string> { "v2a", "v2b" },
    [3] = null
});

Debug.Assert(q == new Uri(
    "https://stackoverflow.com/yo?1=v1&2=v2a&2=v2b&3"));

// Test with an anonymous object.
q = uri.WithQuery(new
{
    k1 = "v1",
    k2 = new[] { "v2a", "v2b" },
    k3 = new List<string> { "v3" },
    k4 = true,
    k5 = null as Queue<string>
});

Debug.Assert(q == new Uri(
    "https://stackoverflow.com/yo?k1=v1&k2=v2a&k2=v2b&k3=v3&k4=True&k5"));

// Keep existing query using a name/value collection.
nvc = HttpUtility.ParseQueryString(uri.Query);
nvc.Add("newKey", "newValue");

q = uri.WithQuery(nvc);
Debug.Assert(q == new Uri(
    "https://stackoverflow.com/yo?newKey=newValue&oldKey=oldValue"));

// Merge two query objects using the RouteValueDictionary.
var an1 = new { k1 = "v1" };
var an2 = new { k2 = "v2" };

q = uri.WithQuery(
    new RouteValueDictionary(an1).Concat(
        new RouteValueDictionary(an2)));

Debug.Assert(q == new Uri(
    "https://stackoverflow.com/yo?k1=v1&k2=v2"));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接