【问题标题】:How to build a query string for a URL in C#?如何在 C# 中为 URL 构建查询字符串?
【发布时间】:2010-10-24 03:58:36
【问题描述】:

从代码调用 Web 资源时的一项常见任务是构建查询字符串以包含所有必要的参数。虽然绝对不是火箭科学,但您需要注意一些漂亮的细节,例如,如果不是第一个参数,则附加 &,对参数进行编码等。

代码很简单,但是有点繁琐:

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,我没找到——这让我想到了以下问题:

你所知道的最优雅的清洁方式是什么?

【问题讨论】:

  • 有点遗憾的是,即使在当前时间点,似乎也没有直接的方式来处理查询字符串。直截了当,我的意思是 OOB、非内部、符合标准的框架类。还是我错过了什么?
  • 你没有错过任何东西。 Querystring 构建是我试图用Flurl 填补的框架中的一个主要空白。
  • 你刚刚让我觉得我应该构建一个.. new UrlBuilder(existing).AddQuery("key", "value").ToString()
  • 这个答案也适用于容易嵌套的对象enter link description here

标签: c# .net url query-string


【解决方案1】:

虽然不优雅,但我选择了一个不使用 NameValueCollecitons 的更简单的版本 - 只是一个围绕 StringBuilder 的构建器模式。

public class UrlBuilder
{
    #region Variables / Properties

    private readonly StringBuilder _builder;

    #endregion Variables / Properties

    #region Constructor

    public UrlBuilder(string urlBase)
    {
        _builder = new StringBuilder(urlBase);
    }

    #endregion Constructor

    #region Methods

    public UrlBuilder AppendParameter(string paramName, string value)
    {
        if (_builder.ToString().Contains("?"))
            _builder.Append("&");
        else
            _builder.Append("?");

        _builder.Append(HttpUtility.UrlEncode(paramName));
        _builder.Append("=");
        _builder.Append(HttpUtility.UrlEncode(value));

        return this;
    }

    public override string ToString()
    {
        return _builder.ToString();
    }

    #endregion Methods
}

根据现有答案,我确保使用HttpUtility.UrlEncode 调用。它是这样使用的:

string url = new UrlBuilder("http://www.somedomain.com/")
             .AppendParameter("a", "true")
             .AppendParameter("b", "muffin")
             .AppendParameter("c", "muffin button")
             .ToString();
// Result: http://www.somedomain.com?a=true&b=muffin&c=muffin%20button

【讨论】:

    【解决方案2】:

    我有一个Uri 的扩展方法:

    • 接受匿名对象:uri.WithQuery(new { name = "value" })
    • 接受string/string 对的集合(例如Dictionary`2)。
    • 接受string/object 对的集合(例如RouteValueDictionary)。
    • 接受NameValueCollections。
    • 按键对查询值进行排序,以便相同的值产生相同的 URI。
    • 支持每个键的多个值,保留其原始顺序。

    文档版本可以在here找到。

    扩展:

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

    【讨论】:

      【解决方案3】:
      // USAGE
      [TestMethod]
      public void TestUrlBuilder()
      {
          Console.WriteLine(
              new UrlBuilder("http://www.google.com?A=B")
                  .AddPath("SomePathName")
                  .AddPath("AnotherPathName")
                  .SetQuery("SomeQueryKey", "SomeQueryValue")
                  .AlterQuery("A", x => x + "C"));
      }
      

      输出:

      http://www.google.com/SomePathName/AnotherPathName?A=BC&SomeQueryKey=SomeQueryValue

      代码;你们都可以在某个地方感谢我,不知何故:D

      using System;
      using System.Collections.Generic;
      using System.Linq;
      using System.Web;
      
      // By Demetris Leptos
      namespace TheOperator.Foundation.Web
      {
          public class UrlBuilder
          {
              public string Scheme { get; set; }
      
              public string Host { get; set; }
      
              public int? Port { get; set; }
      
              public List<string> Paths { get; set; }
      
              public SortedDictionary<string, string> QueryPairs { get; set; }
      
              public UrlBuilder(string url)
              {
                  this.Paths = new List<string>();
                  this.QueryPairs = new SortedDictionary<string, string>();
      
                  string path = null;
                  string query = null;
                  Uri relativeUri = null;
                  if (!Uri.TryCreate(url, UriKind.Relative, out relativeUri))
                  {
                      var uriBuilder = new UriBuilder(url);
                      this.Scheme = uriBuilder.Scheme;
                      this.Host = uriBuilder.Host;
                      this.Port = uriBuilder.Port;
                      path = uriBuilder.Path;
                      query = uriBuilder.Query;
                  }
                  else
                  {
                      var queryIndex = url.IndexOf('?');
                      if (queryIndex >= 0)
                      {
                          path = url.Substring(0, queryIndex);
                          query = url.Substring(queryIndex + 1);
                      }
                      else
                      {
                          path = url;
                      }
                  }
                  this.Paths.AddRange(path.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries));
                  if (query != null)
                  {
                      var queryKeyValuePairs = HttpUtility.ParseQueryString(query);
                      foreach (var queryKey in queryKeyValuePairs.AllKeys)
                      {
                          this.QueryPairs[queryKey] = queryKeyValuePairs[queryKey];
                      }
                  }
              }
      
              public UrlBuilder AddPath(string value)
              {
                  this.Paths.Add(value);
                  return this;
              }
      
              public UrlBuilder SetQuery(string key, string value)
              {
                  this.QueryPairs[key] = value;
                  return this;
              }
      
              public UrlBuilder RemoveQuery(string key)
              {
                  this.QueryPairs.Remove(key);
                  return this;
              }
      
              public UrlBuilder AlterQuery(string key, Func<string, string> alterMethod, bool removeOnNull = false)
              {
                  string value;
                  this.QueryPairs.TryGetValue(key, out value);
                  value = alterMethod(value);
                  if (removeOnNull && value == null)
                  {
                      return this.RemoveQuery(key);
                  }
                  else
                  {
                      return this.SetQuery(key, value);
                  }
              }
      
              public override string ToString()
              {
                  var path = !string.IsNullOrWhiteSpace(this.Host)
                      ? string.Join("/", this.Host, string.Join("/", this.Paths))
                      : string.Join("/", this.Paths);
                  var query = string.Join("&", this.QueryPairs.Select(x => string.Concat(x.Key, "=", HttpUtility.UrlEncode(x.Value))));
                  return string.Concat(
                      !string.IsNullOrWhiteSpace(this.Scheme) ? string.Concat(this.Scheme, "://") : null,
                      path,
                      !string.IsNullOrWhiteSpace(query) ? string.Concat("?", query) : null);
              }
          }
      }
      

      【讨论】:

        【解决方案4】:

        我采用了 DSO 提出的解决方案(2011 年 8 月 2 日 7:29 回答),他的解决方案不需要使用 HttpUtility。但是,根据Dotnetpearls 中发布的一篇文章,使用字典比使用 NameValueCollection 更快(在性能上)。这是 DSO 的解决方案修改为使用 Dictionary 代替 NameValueCollection。

            public static Dictionary<string, string> QueryParametersDictionary()
            {
                var dictionary = new Dictionary<string, string>();
                dictionary.Add("name", "John Doe");
                dictionary.Add("address.city", "Seattle");
                dictionary.Add("address.state_code", "WA");
                dictionary.Add("api_key", "5352345263456345635");
        
                return dictionary;
            }
        
            public static string ToQueryString(Dictionary<string, string> nvc)
            {
                StringBuilder sb = new StringBuilder();
        
                bool first = true;
        
                foreach (KeyValuePair<string, string> pair in nvc)
                {
                        if (!first)
                        {
                            sb.Append("&");
                        }
        
                        sb.AppendFormat("{0}={1}", Uri.EscapeDataString(pair.Key), Uri.EscapeDataString(pair.Value));
        
                        first = false;
                }
        
                return sb.ToString();
            }
        

        【讨论】:

          【解决方案5】:

          在 dotnet core 中,QueryHelpers.AddQueryString() 将接受 IDictionary 键值对。为了节省一些内存分配和 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);
          

          【讨论】:

          【解决方案6】:

          查询字符串可以通过以下方式添加到 URL:

          1. 创建名称值集合对象
          2. 将查询字符串项及其值添加到此对象
          3. 将此名称值集合对象编码为url,代码在下面的链接中提供

          https://blog.codingnovice.com/blog

          public ActionResult Create()
          {
              //declaring name value collection object
              NameValueCollection collection = new NameValueCollection();
          
              //adding new value to the name value collection object
              collection.Add("Id1", "wwe323");
              collection.Add("Id2", "454w");
              collection.Add("Id3", "tyt5656");
              collection.Add("Id4", "343wdsd");
          
              //generating query string
              string url = GenerateQueryString(collection);
          
              return View();
          }
          
          private string GenerateQueryString(NameValueCollection collection)
          {
              var querystring = (
                  from key in collection.AllKeys
                  from value in collection.GetValues(key)
                  select string.Format("{0}={1}",
                      HttpUtility.UrlEncode(key),
                      HttpUtility.UrlEncode(value))
              ).ToArray();
              return "?" + string.Join("&", querystring);
          }
          

          【讨论】:

            【解决方案7】:

            另一种方法是创建返回完整 Url 的类 NameValueCollection 的扩展:

            public static class CustomMethods
            {
                public static string ToUrl(this System.Collections.Specialized.NameValueCollection collection)
                {
                    if (collection.Count == 0) return "";
            
                    string completeUrl = "?";
                    for (int i = 0; i < collection.Count; i++)
                    {
                        completeUrl += new Page().Server.UrlEncode(collection.GetKey(i)) + "=" + new Page().Server.UrlEncode(collection.Get(i));
                        if ((i + 1) < collection.Count) completeUrl += "&";
                    }
            
                    return completeUrl;
                }
            }
            

            然后,您可以使用您的新方法,例如:

            System.Collections.Specialized.NameValueCollection qString = new System.Collections.Specialized.NameValueCollection();
            qString.Add("name", "MyName");
            qString.Add("email", "myemail@test.com");
            qString.ToUrl(); //Result: "?name=MyName&email=myemail%40test.com"
            

            【讨论】:

              【解决方案8】:

              很好奇没有人提到过 AspNet.Core 中的 QueryBuilder。

              当您的查询具有重复键(如 ?filter=a&amp;filter=b)时会很有帮助

                          var qb = new QueryBuilder();
                          qb.Add("filter", new string[] {"A", "B"};
              

              然后你只需将 qb 添加到 URI 中,它会自动转换为字符串。

              https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.extensions.querybuilder?view=aspnetcore-5.0

              【讨论】:

              • 这里的答案太多了,在当前使用 .NET Core 开发的时代,花了一段时间才找到正确的答案。
              • 似乎无法在 dotnet core 3.1 中访问它。未找到命名空间导入并尝试添加 nuget 包 nuget.org/packages/Microsoft.AspNetCore.App.Ref/3.1.10 导致还原错误 error: NU1213: The package Microsoft.AspNetCore.App.Ref 3.1.10 has a package type DotnetPlatform that is incompatible with this project. error: Package 'Microsoft.AspNetCore.App.Ref' is incompatible with 'all' frameworks in project
              • 我认为该软件包仅在 dotnet 5 中可用。
              【解决方案9】:

              这里有很多很好的答案,但对于那些使用现代 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
              

              希望你们中的一些人会觉得这既漂亮又优雅。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2012-01-17
                • 1970-01-01
                • 1970-01-01
                • 2013-06-10
                • 2018-12-26
                相关资源
                最近更新 更多