Query String Params Collection
Uri and UriBuilder are useful utilities for constructing valid HTTP URL query string parameters. In the web domain of discourse I find I'm hinging a lot of logic on query strings which I am forced to deal with as strings. This is a pointless conversion from Request.QueryString since they are clear cut collections of key/value pairs with clear cut standard encoding rules. So I made a wrapper to extend the SerializableDictionary class from a previous post entry, which will represent the collection of query parameters to append to, modify and extract from a valid URL string.
We'll first extend SerializableDictionary class and make it "nullable" by overriding the [] operator as with DictionaryNullable logic. This may or may not cause performance issues but we should never be dealing with sets of URL query parameters longer than 10-12 items (depending on the hard character limit of the HTTP header to which the URL string contributes - off the top of my head that should be at maximum 255 characters but you can verify that on wikipedia). Such a small collection should not add more than a few milliseconds to parse and reference using the indexing override
public class UriQry : SerializableDictionary<string, string> { public UriQry() { } public UriQry(string name, string value) { this[name] = value; } public new string this[string key] { get { if (ContainsKey(key)) return base[key]; return null; } set { if (ContainsKey(key)) base[key] = value; else Add(key, value); } } }
The next thing we want to add is an easy way to convert an object instance of UriQry type to and from URL string values.
public override string ToString() { return Keys .Aggregate(string.Empty, (current, key) => current + String.Format("{0}={1}&", HttpUtility.UrlEncode(key, Encoding.ASCII), HttpUtility.UrlEncode(this[key], Encoding.ASCII))) .TrimEnd('&'); } public static UriQry FromString(string qStr) { var obj = new UriQry(); if (!string.IsNullOrEmpty(qStr)) { string str = HttpUtility.UrlDecode(qStr, Encoding.ASCII).Replace("&", "&").TrimStart(new char[] { '?' }); foreach (string keyVal in str.Split(new char[] { '&' })) { string[] pair = keyVal.Split(new char[] { '=' }); if (pair.Length <= 0) { obj[keyVal] = ""; } else if (pair.Length <= 1) { obj[pair[0]] = ""; } else { string val = ""; for (int i = 1; i < pair.Length; i++) { val = string.Concat(val, pair[i]); } obj[pair[0]] = val; } } } return obj; }
Equipped with the above-defined class we can set up a trivial use case:
var uriQry = new UriQry{{"paramName", "paramValue"}}; Console.WriteLine(uriQry); //paramName=paramValue uriQry = UriQry.FromString("?q=buy+stuff&sId=08130198230198239&thing=Hello World!!!"); Console.WriteLine(uriQry); //q=buy+stuff&sId=08130198230198239&thing=Hello+World!!!
OK so far so good. But the real usefulness would come in having the ability to combine existing Uri and UriBuilder to take advantage of this utility.
So we'll add a Combine method to the UriQry class to concatenate 2 sets of UriQry collections and return a UriQry instance with a combined collection. I'm sure there's a cleaner way to do this by extending IEnumerable instead of Dictionary<string,string> but this is just my way of skinning the cat:
public UriQry Combine(UriQry qPairs) { UriQry ret = new UriQry(); foreach (KeyValuePair<string, string> keyValuePair in qPairs) { ret[keyValuePair.Key] = keyValuePair.Value; } return Convert(ret.ToString(), true); } public UriQry Convert(string qStr) { return Convert(qStr, false); } public UriQry Convert(string qStr, bool append) { if (!append) Clear(); UriQry tmp = FromString(qStr); foreach (KeyValuePair<string, string> pair in tmp) { this[pair.Key] = pair.Value; } return this; }
Additionally, we are going to extend Uri and UriBuilder classes with utility methods to append query string params:
public static class UriBuilderExtensions { public static UriBuilder AppendQuery(this UriBuilder uriBuilder, Dictionary<string, string> paramss) { uriBuilder.Query = UriQry .FromString(uriBuilder.Query) .Combine(new UriQry { paramss }) .ToString(); return uriBuilder; } } public static class UriExtensions { public static Uri AppendQuery(this Uri uri, Dictionary<string, string> paramss) { return new UriBuilder(uri).AppendQuery(paramss).Uri; } }
In a similar vein, we can implement a Combine method as a Uri extension to take advantage of the extensions:
public static class UriExtensions { public static Uri AppendQuery(this Uri uri, Dictionary<string, string> paramss) { return new UriBuilder(uri).AppendQuery(paramss).Uri; } public static Uri Combine(this Uri absBaseUri, params string[] relativeUris) { Uri bUri = new Uri(absBaseUri.PathAndQuery, UriKind.Relative); var ret = relativeUris.Aggregate(bUri, (current, relativeUri) => current.Combine(new Uri(relativeUri, UriKind.Relative))); return new Uri(absBaseUri, ret); } public static Uri Combine(this Uri relativeBaseUri, Uri relativeUri) { if (relativeBaseUri == null) { throw new ArgumentNullException("relativeBaseUri"); } if (relativeUri == null) { throw new ArgumentNullException("relativeUri"); } string baseUrl = VirtualPathUtility.AppendTrailingSlash(relativeBaseUri.ToString()); string combinedUrl = VirtualPathUtility.Combine(baseUrl, relativeUri.ToString()); return new Uri(combinedUrl, UriKind.Relative); } }
Putting It To Good Use
Here's a Global.asax implementation intercepting the application authentication request event in one (albeit very long) line.
protected void Application_AuthenticateRequest(object sender, EventArgs e) { if (Request.IsPostBack() && !Request.IsAuthentication()) { if (!Request.IsAuthenticated) { var qry = new UriQry { { "isLoginExpired", "yes" } }; var uri = new Uri("http://www.ourSite.com/aspDotNetApp/"); Response.Redirect(uri.Combine(FormsAuthentication.LoginUrl).AppendQuery(qry).PathAndQuery, true); } } }
Another example is with a requirement to preserve and clone query string parameters from an arbitrary request:
Uri relUri; //... //by virtue of getting data from somewhere //the equivalent of this happens: relUri = new Uri("/some/path/?a=shit&ton=of&dirty=data¶ms=4", UriKind.Relative); //... UriBuilder uriBuilder = new UriBuilder(new Uri(new Uri("http://domain.com"), relUri)); uriBuilder.Query = UriQry.FromString(uriBuilder.Query).Combine(new UriQry { { "username", "BobRoberts" }, { "password", "01234" } }).ToString(); Console.WriteLine(uriBuilder.Uri.PathAndQuery);
Another straightforward example making code more legible (in combination with HttpRemoteCall class from another post):
int postType; string dataId; string userId; bool isComment; //... var stemUrl = new Uri("http://domain.com/").Combine("/app/approve.ashx", true); UriQry qry = new UriQry { new Dictionary<string, string> { {"type", postType.ToString()}, {"id", dataId}, {"usr", userId}, {"comment", isComment ? "yes" : "no"}, } }; var webReq = new HttpRemoteCall(stemUrl.AppendQuery(qry).AbsoluteUri); webReq.Create(); var webResponse = webReq.Response();
Complete code:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Util; using System.Web; public class UriQry : SerializableDictionary<string, string> { public UriQry() { } public UriQry(string name, string value) { this[name] = value; } public new string this[string key] { get { if (ContainsKey(key)) return base[key]; return null; } set { if (ContainsKey(key)) base[key] = value; else Add(key, value); } } public override string ToString() { return Keys .Aggregate(string.Empty, (current, key) => current + String.Format("{0}={1}&", HttpUtility.UrlEncode(key, Encoding.ASCII), HttpUtility.UrlEncode(this[key], Encoding.ASCII))) .TrimEnd('&'); } public UriQry Combine(UriQry qPairs) { UriQry ret = new UriQry(); foreach (KeyValuePair<string, string> keyValuePair in qPairs) { ret[keyValuePair.Key] = keyValuePair.Value; } return Convert(ret.ToString(), true); } public UriQry Convert(string qStr) { return Convert(qStr, false); } public UriQry Convert(string qStr, bool append) { if (!append) Clear(); UriQry tmp = FromString(qStr); foreach (KeyValuePair<string, string> pair in tmp) { this[pair.Key] = pair.Value; } return this; } public static UriQry FromString(string qStr) { var obj = new UriQry(); if (!string.IsNullOrEmpty(qStr)) { string str = HttpUtility.UrlDecode(qStr, Encoding.ASCII).Replace("&", "&").TrimStart(new char[] { '?' }); foreach (string keyVal in str.Split(new char[] { '&' })) { string[] pair = keyVal.Split(new char[] { '=' }); if (pair.Length <= 0) { obj[keyVal] = ""; } else if (pair.Length <= 1) { obj[pair[0]] = ""; } else { string val = ""; for (int i = 1; i < pair.Length; i++) { val = string.Concat(val, pair[i]); } obj[pair[0]] = val; } } } return obj; } } public static class UriBuilderExtensions { public static UriBuilder AppendQuery(this UriBuilder uriBuilder, Dictionary<string, string> paramss) { uriBuilder.Query = UriQry .FromString(uriBuilder.Query) .Combine(new UriQry { paramss }) .ToString(); return uriBuilder; } } public static class UriExtensions { public static Uri AppendQuery(this Uri uri, Dictionary<string, string> paramss) { return new UriBuilder(uri).AppendQuery(paramss).Uri; } public static Uri Combine(this Uri absBaseUri, params string[] relativeUris) { Uri bUri = new Uri(absBaseUri.PathAndQuery, UriKind.Relative); var ret = relativeUris.Aggregate(bUri, (current, relativeUri) => current.Combine(new Uri(relativeUri, UriKind.Relative))); return new Uri(absBaseUri, ret); } public static Uri Combine(this Uri relativeBaseUri, Uri relativeUri) { if (relativeBaseUri == null) { throw new ArgumentNullException("relativeBaseUri"); } if (relativeUri == null) { throw new ArgumentNullException("relativeUri"); } string baseUrl = VirtualPathUtility.AppendTrailingSlash(relativeBaseUri.ToString()); string combinedUrl = VirtualPathUtility.Combine(baseUrl, relativeUri.ToString()); return new Uri(combinedUrl, UriKind.Relative); } }











