May 08

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+AM
  queryString.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:

  1. PageUrlAttribute(string pageUrl) – Supplies the application relative page url used by the Navigate() method.
  2. 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 string
    public override string ToString()
    {
      NameValueCollection collection = new NameValueCollection();
      // Get properties
      PropertyInfo[] 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 string
        object value = property.GetValue(this, null);
        // No need to pass null values to query string
        if (value == null) continue;
        string stringValue = value.ToString();
        if (queryParameter.Encrypt)
        {
          // Encrypt value
          stringValue = 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/208427
      Debug.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 class
    public void FromQueryString(string queryString)
    {
      NameValueCollection collection = FromString(queryString);
      // Get properties
      PropertyInfo[] 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 default
        if (collection[queryParameter.Key] == null) continue;
        // Set properties from query string
        string value = HttpUtility.UrlDecode(collection[queryParameter.Key]);
        if (queryParameter.Encrypt)
        {
          // Decrypt value
          value = 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 attribute
        object[] 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 URL
    private 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
}
Tagged with:
preload preload preload