Download TypedQueryStrings Vs.Net 2010 Solution
Introduction
I’ve always been bothered by the monotony of passing parameters to web pages via query strings as well as the potential for defects. For example, there may be three web pages in your application that all pass parameters to one page. Many times I’ve seen the query string construction logic duplicated across the sending pages. Then the receiving page must also have a matching signature. There’s the overhead of making sure that the parameter key names match across the pages, the proper values are passed, encoded properly, optionally encrypted, casting is done, and null checks are put on optional parameters. I wonder why we’ve put up with all these issues for so long? And why does the Asp.net framework only include a read-only dictionary (NameValueCollection) for Request.QueryString that offers no help for building query strings? There are several great QueryStringBuilder utility classes that have been written to fill this gap. These make creating query strings as easy as adding your keys and values to a dictionary and calling “ToString()”. One of my favorites is BradVin’s chainable query string builder.
//output : "?id=123&user=brad&sessionId=ABC"
string strQuery = QueryString.Current.Add("id", "123").Add("user", "brad").Add("sessionId", "ABC").Remove("action").ToString();
This library helps create properly formed query strings easily. It’s especially useful when having to remove and add parameters which is especially painful to do with string concatenation. It also allows for encrypting and decrypting sensitive parameters.
What’s So Bad About Query Strings?
You may be thinking that working with query strings is easy and you’re happy with doing them manually. Let me remind you of some of the problems that can arise.
- Duplicate query string construction code gets out of sync
- It can be difficult to remember what parameters a page takes and which are optional
- Time consuming to find all the pages that pass parameters when renaming a key name or deciding to encrypt the value
- It’s easy to generate a bad query string structure when attempting to remove certain parameters from an existing query string
- Forgetting to url encode certain values leads to subtle bugs that are inconsistent and hard to reproduce
- Null handling logic for optional parameters on the receiving page is tedious to write and distracts from the important logic
Dino Esposito, contributor for MSDN magazine wrote an article trying to solve some of the above issues. His solution was to create an HTTP handler to validate that a query string for each page has the expected parameter keys and value types described in an xml document.
Validating ASP.NET Query Strings – Dino Esposito
Let’s Dream
What if we could define each of the parameters the page would accept, their types, if they were to be encrypted, mark them as optional, and supply a default? What if the sending pages could instantiate a typed parameter class for the specific page, set the properties to be passed, and call a Navigate() method to redirect to the page? Then the receiving page would create the same object and could just read each parameter value from the object’s properties?
This is what drove me to the design of the TypedQueryString abstract class. You can simply derive from this class and add properties for each of your query parameters. You must attribute the properties with the query parameter key and encryption flag. You can then use it as follows:
TypedQueryString Usage
// Creating TypedQueryString from page2 (sender):Page1QueryString qs = new Page1QueryString() { UserId = 5, PageTitle = "Page 1 Title" };string queryString = qs.ToString();// queryString: http://MySite.com/Web/Page1.aspx?u=il%2bgAhwr%2boY%3d&t=Page+1+Title&s=1%2f1%2f0001+12%3a00%3a00+AMqueryString.Navigate();// Reading TypedQueryString from page1 (receiver):Page1QueryString qs2 = new Page1QueryString();qs2.FromQueryString(queryString);int userId = qs2.UserId;string pageTitle = qs2.PageTitle;DateTime startDate = qs2.StartDate;
Deriving from TypedQueryString Class
For each web page that receives query parameters, create a class for your parameters deriving from TypedQueryString. This can be created as a nested class inside your receiving Asp.net web page. Notice there are custom attributes defined:
- PageUrlAttribute(string pageUrl) – Supplies the application relative page url used by the Navigate() method.
- QueryStringParameter(string key, bool encrypt) – Maps the key to the property value and indicates if parameter value should be encrypted.
[PageUrl("~/Web/Page1.aspx")]public class Page1QueryString : TypedQueryString{[QueryStringParameter("u", true)]public int UserId { get; set; }[QueryStringParameter("t", false)]public string PageTitle { get; set; }[QueryStringParameter("s", false)]public DateTime StartDate { get; set; }}
How It Works
Internally the TypedQueryString class uses reflection to get and set the property values and create the query string. The parameter values are optionally encrypted and then encode for the query string.
Coming Features
Next, I would like to be able to attribute the parameters as optional and provide a default value. If the parameter is not specified as optional and the property is not set an assertion should be raised at runtime so the developer can know they didn’t provide a required parameter. The difficulty is in knowing if the property has been set or not.
TypedQueryString.cs
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Web;using System.Collections.Specialized;using System.Web.UI;using System.ComponentModel;using System.Reflection;using System.Diagnostics;using Utils.Cryptography;namespace Util.Web{public abstract class TypedQueryString{private const int MAX_URL_LENGTH = 2083;private const int AVG_PATH_LENGTH = 60;// Reads properties in derived class and outputs query stringpublic override string ToString(){NameValueCollection collection = new NameValueCollection();// Get propertiesPropertyInfo[] properties = this.GetType().GetProperties();foreach (var property in properties){// Get query parameter attribute on property.object[] attributes = property.GetCustomAttributes(typeof(QueryStringParameterAttribute), false);if (attributes.Count() == 0) continue;QueryStringParameterAttribute queryParameter = attributes[0] as QueryStringParameterAttribute;if (queryParameter == null) continue;// Read each attribute and create query stringobject value = property.GetValue(this, null);// No need to pass null values to query stringif (value == null) continue;string stringValue = value.ToString();if (queryParameter.Encrypt){// Encrypt valuestringValue = Cryptography.Encrypt(stringValue);}if (collection[queryParameter.Key] != null)Debug.Assert(false, "Duplicate query parameter key cannot be added for key: " + queryParameter.Key);collection.Add(queryParameter.Key, stringValue);}StringBuilder builder = new StringBuilder();for (var i = 0; i < collection.Keys.Count; i++){if (!string.IsNullOrEmpty(collection.Keys[i])){foreach (string val in collection[collection.Keys[i]].Split(','))builder.Append((builder.Length == 0) ? "?" : "&").Append(HttpUtility.UrlEncodeUnicode(collection.Keys[i])).Append("=").Append(HttpUtility.UrlEncodeUnicode(val));}}string url = builder.ToString();// Max URL length in IE: http://support.microsoft.com/kb/208427Debug.Assert(url.Length < (MAX_URL_LENGTH - AVG_PATH_LENGTH), string.Format("Url length should be less than {0} for Internet Explorer", MAX_URL_LENGTH));return url;}public void FromQueryString(){if (HttpContext.Current != null){FromQueryString(HttpContext.Current.Request.QueryString);}}public void FromQueryString(NameValueCollection queryString){FromQueryString(queryString.ToString());}// Sets the property values in the derived classpublic void FromQueryString(string queryString){NameValueCollection collection = FromString(queryString);// Get propertiesPropertyInfo[] properties = this.GetType().GetProperties();foreach (var property in properties){// Get query parameter attribute on property.object[] attributes = property.GetCustomAttributes(typeof(QueryStringParameterAttribute), false);if (attributes.Count() == 0) continue;QueryStringParameterAttribute queryParameter = attributes[0] as QueryStringParameterAttribute;if (queryParameter == null) continue;// Will be null if using defaultif (collection[queryParameter.Key] == null) continue;// Set properties from query stringstring value = HttpUtility.UrlDecode(collection[queryParameter.Key]);if (queryParameter.Encrypt){// Decrypt valuevalue = Cryptography.Decrypt(value);}Type propertyType = property.PropertyType;if (property.PropertyType.IsGenericType && property.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)){propertyType = Nullable.GetUnderlyingType(property.PropertyType);}object castedValue = Convert.ChangeType(value, propertyType);property.SetValue(this, castedValue, null);}}public void Navigate(){if (HttpContext.Current != null){Page page = HttpContext.Current.CurrentHandler as Page;if (page != null){string path = page.ResolveUrl(PageUrl) + this.ToString();HttpContext.Current.Response.Redirect(path);}}}public string PageUrl{get{string url = null;// Get page from class description attributeobject[] attributes = this.GetType().GetCustomAttributes(typeof(PageUrlAttribute), false);if (attributes.Count() > 0){PageUrlAttribute pageUrl = attributes[0] as PageUrlAttribute;if (pageUrl != null){url = pageUrl.PageUrl;}}return url;}}private NameValueCollection FromString(string queryString){NameValueCollection collection = new NameValueCollection();if (string.IsNullOrEmpty(queryString)) return collection;foreach (string keyValuePair in ExtractQuerystring(queryString).Split('&')){if (string.IsNullOrEmpty(keyValuePair)) continue;string[] split = keyValuePair.Split('=');collection.Add(split[0],split.Length == 2 ? split[1] : "");}return collection;}// extracts a querystring from a full URLprivate string ExtractQuerystring(string s){if (!string.IsNullOrEmpty(s)){if (s.Contains("?"))return s.Substring(s.IndexOf("?") + 1);}return s;}}#region Custom Attributes[AttributeUsage(AttributeTargets.Property, AllowMultiple=true)]public class QueryStringParameterAttribute : System.Attribute{public QueryStringParameterAttribute(string key){Key = key;Encrypt = false;IsRequired = true;DefaultValue = null;}public QueryStringParameterAttribute(string key, bool encrypt){Key = key;Encrypt = encrypt;IsRequired = true;DefaultValue = null;}public QueryStringParameterAttribute(string key, bool encrypt, bool isRequired, object defaultValue){Key = key;Encrypt = encrypt;IsRequired = isRequired;DefaultValue = defaultValue;}public string Key { get; set; }public bool Encrypt { get; set; }public bool IsRequired { get; set; }public object DefaultValue { get; set; }}[AttributeUsage(AttributeTargets.Class, AllowMultiple=false)]public class PageUrlAttribute : System.Attribute{public PageUrlAttribute(string relativeUrl){PageUrl = relativeUrl;}public string PageUrl { get; set; }}#endregion}