Automatic Parameter Type-Checking

Author: Will Shaver
Date: August 6th, 2007

Introduction

Download Example Files: ValidatedParameter.exe Self Extracting ValidatedParameter.rar

In this tutorial I’m going to cover creating a custom QueryStringParameters class, which will help us validate query string parameters. A common problem faced by all web developers is making sure that the URL strings supplied by the client browser are correct. (Never trust the web client.) For controls that are directly bound to a database (DataBound Controls) one option for selecting which row, page, or item to display is by using a QueryStringParameter. Unfortunately the default parameter causes an exception to occur if the wrong data type is supplied. While this problem can be solved with an on-page solution, I’ve going to cover in this tutorial how to create a more global solution that can apply to all of the query string parameters in an entire site.

Step 1: Setup The Project

Hopefully you’ve read either my last tutorial or some of the asp.net/learn tutorials on dealing with databases in asp.net. I’m using the same basic Northwind database setup. Add a DataSet xsd file to the project in the App_Code directory. I’m using the products table, but we’ll only need one row at a time for this so use the query select * from Products where ProductID = @ProductID. Because we won’t be doing any updating or inserting, go into the advanced options and uncheck the “Generate Insert…” option.

Figure 1

Figure 1: The Products Table Setup – Turn off the Advanced Options

Going back to the Default.aspx page, add an ObjectDataSource and bind it to the ProductsTableAdapter created automatically from the DataSet1.xsd compilation. (Note that you could create a ProductsBLL class and bind it to that instead as I did in the last tutorial, I’m skipping that step for simplicity.)

Figure 2

Figure 2: Binding the ObjectDataSource to the ProductsTableAdapter

The next wizard pane allows selection of what to use for the ProductID SQL parameter that is part of the products table’s select query. Using a QueryString will allow the ProductID to be passed from the browser’s URL directly to the SQL string.

Figure 3

Figure 3: Use a QueryString for choosing a ProductID

Now add a DetailsView to the page and specify the ObjectDataSource that is now linked to the products table.

Figure 4

Figure 4: Select The ObjectDataSource for the DetailsView

Step 2: Breaking Up The Band

Now debug the application in your browser. It should display the first entry in the products table. Manually type in ?ProductID=5 and it should display a different product from the Products table. Different products can be selected by changing the number in the query string.

Figure 5

Figure 5: Select a Product by Modifying the QueryString

Now we’ll attempt to crash the server by specifiying a known bad value. Change the number after ProductID to something that won’t convert to an Int32, such as ‘xxx’, ‘brown cow’, or ‘drop table products’. The browser should now display a System.FormatException, “Input string was not in correct format.”

Figure 6

Figure 6: Attempting to Crash the Server

Step 3: Doing Homework on the Problem

Examining the contents of the Stack Trace is somewhat revealing:



   [FormatException: Input string was not in a correct format.]
   System.Number.StringToNumber(String str, NumberStyles options, NumberBuffer& number,
            NumberFormatInfo info, Boolean parseDecimal) +2725283
   System.Number.ParseInt32(String s, NumberStyles style, NumberFormatInfo info) +102
   System.String.System.IConvertible.ToInt32(IFormatProvider provider) +43
   System.Convert.ChangeType(Object value, TypeCode typeCode, IFormatProvider provider) +293
   System.Web.UI.WebControls.Parameter.GetValue(Object value, String defaultValue, TypeCode type,
            Boolean convertEmptyStringToNull, Boolean ignoreNullableTypeChanges) +257
   System.Web.UI.WebControls.Parameter.get_ParameterValue() +91
   System.Web.UI.WebControls.ParameterCollection.GetValues(HttpContext context, Control control) +282
   ...

It would appear that somewhere in System.Number.StringToNumber the server attempts to convert ‘xxx’ to an Int32 and fails. One solution which Erwyn van der Meer explains very well in his post: Adding Input Validation to Declarative Query String Parameters in ASP.NET 2.0 is to hook the OnSelecting event for the ObjectDataSource. This solution allows us to attempt a parse of the ProductID before our parameters try and convert it.

This works great for a single page, but for a large site with multiple pages that have potential parameters this could become quite tedious; a different event handler would need to be created for every ObjectDataSource, with different parameter names and data type validation.

A better solution can usually be found by moving the implementation higher up in the event callstack. To determine where to implement the solution, start by looking at the exception stack trace. The trace reveals that the conversion occurs after the Parameter class returns the value, meaning that no type checking is occurring in the parameter class. I found this odd, as a type is clearly defined in the ObjectDataSource‘s SelectParameters. Reviewing the automatically generated ObjectDataSource code in the Default.aspx file shows that the Type on the QueryStringParameter is indeed set to Int32.


        <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" OldValuesParameterFormatString="original_{0}"
            SelectMethod="GetData" TypeName="DataSet1TableAdapters.ProductsTableAdapter">
            <SelectParameters>
                <asp:QueryStringParameter DefaultValue="1" Name="ProductID" QueryStringField="ProductID"
                    Type="Int32" />
            </SelectParameters>
        </asp:ObjectDataSource>

To learn more about this problem I whipped out Lutz’ Reflector for .NET, and found the QueryStringParameter class. Open the reflector, click the search glass, then specify QueryStringParameter. I find that clicking on the equals sign (=) to the right of the search box can help narrow down the results if I’m searching for a very generic term. Double click on the result.

Figure 7

Figure 7: Finding the QueryStringParameter in the Reflector

Now right click and disassemble the class. From here all of the functions are accessible by clicking on them. Right clicking and choosing Analyze will reveal what classes call this function, and what classes it relies on. From the exception that was displayed when the junk input value was provided, the page builder calls the GetValues function of the ParameterCollection to start this process. Browsing though the disassembled classes for QueryStringParameter, Parameter and ParameterCollection, it becomes clear that the GetValue function of the Parameter class is what does the actual work of returning the proper object. This in turn uses the ViewState["ParameterValue"] as the current value. Through a convoluted series of events, methods, properties and witchcraft, this works its way back to the QueryStringParameter‘s Evaluate method.

Figure 8

Figure 8: Disassemble the QueryStringParameter class

In the Evaluate code the query string field is returned as an object without any type checking or validation. What if this code checked to make sure that the value could be converted to the type expected by the SQL statement? Remember that in the ObjectDataSource‘s QueryStringParameter definition back in the aspx file that there is a Type specified as Int32.


        protected override object Evaluate(HttpContext context, Control control)
        {
            if ((context != null) && (context.Request != null))
            {
                return context.Request.QueryString[this.QueryStringField];
            }
            return null;
        }

Step 4: Sub-Classing FTW

The best way to accomplish the changing of this code is to create a subclass of it, and change the code for this method to fit our needs. Back in Visual Studio, add a folder called CustomControls to the App_Code directory, and in it create a new class called ValidatedQueryStringParameter. I find that a great way to subclass is to start with the actual code for that class, and then modify it to fit my needs. To get the actual code, click on Expand Methods in the disassembled QueryStringParameter class.

Figure 9

Figure 9: Expanding the QueryStringParameter’s Methods

Copy and paste the code onto our class, leaving the standard includes at the top of the file. Be sure and include the DefaultProperty and other attributes above the class definition. Change the inheritance from Parameter to QueryStringParameter. Because the only method that needs to be updated is Evaluate, remove all of the other methods and properties from the class. A quick compilation of the file will indicate that a couple of classes don’t exist in the current context. Add System.ComponentModel and System.Security.Permissions to the using section. Wrap this whole thing in a namespace called CustomControls. (I’ll cover why in the next section.)

At this point your code should look something similar to this:


using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

using System.ComponentModel;
using System.Security.Permissions;

namespace CustomControls
{
    [DefaultProperty("QueryStringField"), AspNetHostingPermission(SecurityAction.LinkDemand,
        Level = AspNetHostingPermissionLevel.Minimal), AspNetHostingPermission(SecurityAction.InheritanceDemand,
        Level = AspNetHostingPermissionLevel.Minimal)]
    public class ValidatedQueryStringParameter : QueryStringParameter
    {
        protected override object Evaluate(HttpContext context, Control control)
        {
            if((context != null) && (context.Request != null))
            {
                return context.Request.QueryString[this.QueryStringField];
            }
            return null;
        }
    }
}

To use this masterpiece of a control, it must be registered back in the Default.aspx page. There are several different methods for registering a custom control, the method I prefer is via a namespace directive. <%@ Register TagPrefix="cc" Namespace="CustomControls" %>. This can also be done in the Web.config file instead of the page itself. Modify the SelectParameter of the ObjectDataSource1 control to use this custom class instead of the default QueryStringParameter.


        <asp:ObjectDataSource ID="ObjectDataSource1" runat="server" OldValuesParameterFormatString="original_{0}"
            SelectMethod="GetData" TypeName="DataSet1TableAdapters.ProductsTableAdapter">
            <SelectParameters>
                <cc:ValidatedQueryStringParameter DefaultValue="1"
                    Name="ProductID" QueryStringField="ProductID" Type="Int32" />
            </SelectParameters>
        </asp:ObjectDataSource>

Step 5: Validation

The ValidatedQueryStringParameter should now operate exactly like the standard QueryStringParameter that it is inherited from. (To ensure it is calling the new function, place a breakpoint at the return point before debugging the project.) This method now needs to be re-written to perform validation on the parameter prior to returning. The pseudocode logic for this validation will be:


if(can convert)
    return value
else
    return default

Implementing the simple integer validation solution first, add a call to Int32.TryParse before returning from Evaluate, and return the DefaultValue if TryParse fails. The code should look like this:


        protected override object Evaluate(HttpContext context, Control control)
        {
            if((context != null) && (context.Request != null))
            {
                string s = context.Request.QueryString[this.QueryStringField];
                int i;
                if(Int32.TryParse(s, out i))
                    return s;
                return this.DefaultValue;
            }
            return null;
        }

Fire up the debugger and specify a few different values for the ProductID parameter. It should properly revert back to the default value of 1 for any non-integer.

Conclusion

This solution is obviously bare-bones, and in the downloadable code above I’ve changed it to a switch statement for a number of types in the TypeCode enum. It would even be possible to extend this class further to provide an IntegerQueryStringParameter which has MinValue and MaxValue parameters definable in the aspx file. Hopefully this solution has helped you to see the value of avoiding page-specific code in favor of creating custom controls. Anytime you find yourself writing the same code in two separate files, reconsider.

Good Luck

     -Will

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>