如何在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个回答

855

您可以通过调用System.Web.HttpUtility.ParseQueryString(string.Empty)来创建一个新的可写的HttpValueCollection实例,然后将其用作任何NameValueCollection。一旦您添加了所需的值,您可以调用集合上的ToString方法来获取查询字符串,如下所示:

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

queryString.Add("key1", "value1");
queryString.Add("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://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.webutilities.queryhelpers 示例代码:
const string url = "https://customer-information.azure-api.net/customers/search/taxnbr";
var param = new Dictionary<string, string>() { { "CIKey", "123456789" } };

var newUrl = new Uri(QueryHelpers.AddQueryString(url, param));

6
您可以为IDictionary接口创建一个名为ToURLQueryString的扩展方法:public static string ToURLQueryString(this IDictionary dict) { ... } - Roy Tinker
73
这种方法不符合多字节字符的标准,它会将它们编码为%uXXXX而不是%XX%XX。因此产生的查询字符串可能会被Web服务器错误地解释。即使在由HttpUtility.ParseQueryString()返回的内部框架类HttpValueCollection中也有记录。注释称他们保留这种行为是为了向后兼容。 - alex
29
请注意,HttpUtility.ParseQueryString("")和new NameValueCollection()之间有一个重要的区别——只有HttpUtility的结果会覆盖ToString()以生成正确的查询字符串。 - Frank Schwieterman
11
查询字符串中需要多次使用相同参数名称的情况怎么办?例如,“type=10&type=21”。 - Finster
8
使用Add方法可以将多个名称实例添加到查询字符串中。例如:queryString.Add("type", "1"); queryString.Add("type", "2");实际上,始终使用Add方法可能是更好的做法。 - jeremysawesome
显示剩余9条评论

326

如果你深入了解QueryString属性,你会发现它是一个NameValueCollection。当我做类似的事情时,通常都对序列化和反序列化感兴趣,因此我的建议是构建一个NameValueCollection并将其传递给:

using System.Linq;
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 中也有一种超级优雅的方法可以做到这一点...


24
HTTP规范(RFC 2616)没有说明查询字符串可以包含什么内容。 定义通用URI格式的RFC 3986也没有。常用的键值对格式称为“application/x-www-form-urlencoded”,实际上是由HTML定义的,用于将表单数据作为“GET”请求的一部分提交。 HTML5不禁止此格式中每个键具有多个值,并且实际上要求浏览器在页面(错误地)包含具有相同“name”属性的多个字段的情况下生成每个键的多个值。请参见http://goo.gl/uk1Ag。 - Daniel Cassidy
16
我知道我的评论已经过去一年了(答案已经超过两年了!),但是通过使用GetValues(key)方法,NameValueCollection非常支持每个键有多个值。 - Mauricio Scheffer
4
@MauricioScheffer说:但是NameValueCollection不能正确地转换成查询字符串。例如,如果您在WebClient上设置QueryString参数,在同一个键出现多次的情况下,它会变成"path?key=value1,value2"而不是"path?key=value1&key=value2",而后者是一种常见(标准?)的模式。 - David Pope
9
关于一个键对应多个值,我认为在HTML中,如果有一个多选列表框并且选择了多个项目并提交,它们会以David提到的多值格式发送。 - Sam
13
你可能想使用 Uri.EscapeDataString 替代 HttpUtility.UrlEncode,它更具可移植性。请参考 https://dev59.com/oHE85IYBdhLWcg3w3Xhp - PEK
显示剩余6条评论

115

在受到Roy Tinker评论的启发后,我最终使用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;
}

4
好的。已添加到我的内部图书馆中。 :) - Andy
1
你还应该对值进行URL编码。queryString.Add(name, Uri.EscapeDataString(value)); - Ufuk Hacıoğulları
2
感谢您改进这个答案。它修复了多字节字符的问题。 - Ufuk Hacıoğulları
11
顺带一提,这无法使用相对 URL,因为你无法从相对 Uri 实例化 UriBuilder。 - Yuriy Faktorovich
1
我添加了一个删除键,以便不能添加重复项。 https://dotnetfiddle.net/hTlyAd - Paul Totzke
显示剩余9条评论

58

4
这里有很多答案,花了一些时间才找到在当前使用.NET Core开发时正确的答案。 - flq
1
似乎无法在dotnet core 3.1中访问此内容。命名空间导入未找到,并尝试添加nuget包https://www.nuget.org/packages/Microsoft.AspNetCore.App.Ref/3.1.10会导致还原错误`error: NU1213:包Microsoft.AspNetCore.App.Ref 3.1.10具有不兼容于此项目的DotnetPlatform包类型。 错误:包'Microsoft.AspNetCore.App.Ref'与项目中的“所有”框架不兼容。` - GameSalutes
我认为这个包只能在dotnet 5中使用。 - Denis Nutiu
1
该包在dotnet 3.1中可用(早期版本也是如此,如果我没记错的话),但它有一个严重的缺点,即不兼容netstandard2.0,因此对我的类库没有用处。 - Auspex
2
这是正确的现代解决方案(找了一段时间才找到)。此外,可以使用'Microsoft.AspNetCore.Http'命名空间中的类QueryString:https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.querystring.create?view=aspnetcore-6.0 - somedotnetguy
注意:QueryBuilder需要Microsoft.AspNetCore.Http.Extensions包,该包已被弃用且不再维护。 - undefined

50

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!"
});

可选的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

请注意保留HTML标记。

就我所尝试的而言,这是可以让您在查询字符串中添加数组的解决方案!! - Mahyar Mottaghi Zadeh

32
我之前回答过一个类似的问题。基本上,最好的方法是使用类HttpValueCollection,它实际上是ASP.NET的Request.QueryString属性,但不幸的是它在.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

谢谢......我注意到它返回的NameValueCollection有一个ToString()方法,但我无法弄明白为什么它的行为不同。 - calebt
httpValueCollection.ToString() 实际上调用了 httpValueCollection.ToString(true),因此不需要显式添加 true - dav_i
8
HttpValueCollection 是一个内部类,因此您无法实例化它。 - ozba

31

以下是一种扩展方法的流畅/lambda式写法(结合了之前帖子中的概念),支持同一键的多个值。我个人倾向于使用扩展方法而非包装器,因为其他团队成员可以更容易地发现这样的东西。请注意,在编码方法方面存在争议,Stack Overflow上有很多帖子(其中一个)和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());
}

编辑: 支持空值,但您可能需要根据您的特定情况进行调整

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;
}

1
如果任何一个值为空,这将失败。 - Josh Noe
这是错误的,它为每个键值对生成了许多查询字符串。 - Gayan
@GayanRanasinghe:那甚至是什么意思? - Matti Virkkunen

25

这是我提交的晚期作品。因为各种原因,我都不喜欢其他人写的代码,所以我自己写了一个。

这个版本特点如下:

  • 仅使用StringBuilder。没有ToArray()调用或其他扩展方法。虽然它看起来不像其他响应那样美观,但我认为这是一项核心功能,因此效率比具有“流畅”、“单行”代码的隐藏inefficiencies更重要。

  • 处理每个键的多个值。(我自己不需要,但只是为了让Mauricio安心 ;)

  • 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
    

我喜欢这个不使用HttpUtility,因为它在System.Web下,并且不是到处都可用。 - Kugel
1
+1 如果不使用 LINQ 和 HttpUtility。我会创建一个空的 sb 并且抛弃 "bool first" 变量,然后在循环中只需在 sb.AppendFormat() 之前加入 sb.Append(sb.Length == 0 ? "?" : "&") 即可。现在,如果 nvc 为空,则该方法将返回一个空字符串,而不是一个孤独的 "?"。 - Mathew Leger
此答案处理具有多个值的单个参数。例如:?id=1&id=3&id=2&id=9 - Mathemats

23

我需要解决一个便携式类库(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;

这是我使用的技术,并在另一个问题中引用了它https://dev59.com/YmQn5IYBdhLWcg3wGT8X#26744471。唯一的区别是我使用KeyValue对的数组...除了需要对System.Net的引用(正如您所述,它在PCL中可用)之外,这在我的看法中是最简单的方法,而不必包含某些第三方软件包或尝试拼凑一些自制的混乱代码。 - Rostov

14

那么创建一些扩展方法如何?这些方法允许您以流畅的方式添加参数,就像这样:

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

对于基于字符串的简单扩展方法,我给出了一个:+1:。其他答案可能涵盖更多边缘情况,但这对我的情况已经足够了,而且不需要我先构造NameValueCollectionHttpValueCollectionUri。谢谢! - Stanley G.

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