How to build a query string for a URL in C#?

前端 未结 30 2383
借酒劲吻你
借酒劲吻你 2020-11-22 01:55

A common task when calling web resources from a code is building a query string to including all the necessary parameters. While by all means no rocket science, there are so

相关标签:
30条回答
  • 2020-11-22 02:40

    I have an extension method for Uri that:

    • Accepts anonymous objects: uri.WithQuery(new { name = "value" })
    • Accepts collections of string/string pairs (e.g. Dictionary`2).
    • Accepts collections of string/object pairs (e.g. RouteValueDictionary).
    • Accepts NameValueCollections.
    • Sorts the query values by key so the same values produce equal URIs.
    • Supports multiple values per key, preserving their original order.

    The documented version can be found here.

    The extension:

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

    The query parser:

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

    Here are the tests:

    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"));
    
    0 讨论(0)
  • 2020-11-22 02:43

    With the inspiration from Roy Tinker's comment, I ended up using a simple extension method on the Uri class that keeps my code concise and clean:

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

    Usage:

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

    Edit - Standards compliant variant

    As several people pointed out, httpValueCollection.ToString() encodes Unicode characters in a non-standards-compliant way. This is a variant of the same extension method that handles such characters by invoking HttpUtility.UrlEncode method instead of the deprecated HttpUtility.UrlEncodeUnicode method.

    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;
    }
    
    0 讨论(0)
  • 2020-11-22 02:44

    I answered a similar question a while ago. Basically, the best way would be to use the class HttpValueCollection, which ASP.NET's Request.QueryString property actually is, unfortunately it is internal in the .NET framework. You could use Reflector to grab it (and place it into your Utils class). This way you could manipulate the query string like a NameValueCollection, but with all the url encoding/decoding issues taken care for you.

    HttpValueCollection extends NameValueCollection, and has a constructor that takes an encoded query string (ampersands and question marks included), and it overrides a ToString() method to later rebuild the query string from the underlying collection.

    Example:

      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
    
    0 讨论(0)
  • 2020-11-22 02:44
        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());
        }
    
    0 讨论(0)
  • 2020-11-22 02:46

    I needed to solve the same problem for a portable class library (PCL) that I'm working on. In this case, I don't have access to System.Web so I can't use ParseQueryString.

    Instead I used System.Net.Http.FormUrlEncodedContent like so:

    var url = new UriBuilder("http://example.com");
    
    url.Query = new FormUrlEncodedContent(new Dictionary<string,string>()
    {
        {"param1", "val1"},
        {"param2", "val2"},
        {"param3", "val3"},
    }).ReadAsStringAsync().Result;
    
    0 讨论(0)
  • 2020-11-22 02:46

    Same as accepted solution, but transfred to "dot" LINQ syntax...

    private string ToQueryString(NameValueCollection nvc)
    {
        if (nvc == null) return String.Empty;
        var queryParams = 
              string.Join("&", nvc.AllKeys.Select(key => 
                  string.Join("&", nvc.GetValues(key).Select(v => string.Format("{0}={1}", HttpUtility.UrlEncode(key), HttpUtility.UrlEncode(v))))));
        return "?" + queryParams;
    }
    
    0 讨论(0)
提交回复
热议问题