Click here to Skip to main content
15,867,308 members
Articles / Web Development / ASP.NET
Article

Declarative QueryString Parameter Binding

Rate me:
Please Sign up or sign in to vote.
4.89/5 (78 votes)
2 Mar 200410 min read 222.1K   95   35
Describes using reflection to automatically populate member parameters from the Form and Querystring.

Introduction

This article discusses a strategy for automatic population of instance fields and properties on an ASP.NET page from parameters supplied via properties of the Request object (.QueryString, .Form etc...).

The Problem

If you have parameterized ASP.NET pages - i.e. pages that modify their output on the basis of some input from the GET (Request.QueryString) or POST (Request.Form) parts of the request - then you probably spend a lot of time writing parameter retrieval code, which probably started out like this:

C#
protected string FirstName{
    get{ return Request.QueryString["FirstName"]; }
}

...but then you have to deal with the potential of FirstName being null throughout the rest of your code, so you probably supply a default empty value, by rewriting like this (Note: handy IsNull method, we'll be using throughout the code):

protected static string IsNull(string test, string defaultValue){
    return test==null ? defaultValue : test;
}

protected string FirstName_NullSafe{
   get{ return IsNull(Request.QueryString["FirstName"],""); }
}

This approach works fine for non-string types too, you've just got to add your Convert.ToXXX() statement into your property, and decide what to do when the parameter is null - supply a default, or throw an exception:

protected int CustomerID{
    get{
        object o=Request.QueryString["CustomerID"];
        if (o==null)
            throw new 
               ApplicationException("Customer ID is required to be passed");
        else
            try{
                return Convert.ToInt32(o,10);
            }catch(Exception err){
                throw new ApplicationException("Invalid CustomerID", err);
            }
        }
    }
}

Yuk. What started out as a simple accessor is starting to grow a bit. Also that code's running every time we access CustomerID - so that's unnecessary overhead - and it doesn't run till you first access it, so you get halfway through your (expensive?) process before that exception gets thrown.

It's probably about this point that we refactor the whole mess, and do all this work upfront in the Page_Load (or the end of OnInit):

private void Page_Load(object sender, System.EventArgs e) {
    string o;

    // This was the one we set a default for
    FirstName2 =IsNull(Request.QueryString["FirstName"], "");

    // This one is required
    o =Request.QueryString["CustomerID"];
    if (o==null)
        throw new 
          ApplicationException("Customer ID is required to be passed");
    else
        try{
            CustomerID2 = Convert.ToInt32(o,10);
        }catch(Exception err){
            throw new ApplicationException("Invalid CustomerID", err);
        }

        // This one's an enum (just to make life interesting)
        o =Request.QueryString["Gender"];
        if (o==null)
            throw new ApplicationException("Gender is required");
        else
            try{
                Gender =(Gender)Enum.Parse(typeof(Gender), o, true);
            }catch(Exception err){
                throw new ApplicationException("Invalid Gender", err);
            }
}

Now you've only got to do this a couple of times, and there's a clear pattern emerging. Whether you're populating fields or properties (i.e. ViewState wrappers), there's a couple of standard actions going on:

  • Retrieving a value from Request.QueryString (or Request.Form), based on a key (potentially the same as the field name)
  • Throwing an appropriate exception if the value wasn't supplied, or supplying a default
  • Converting the value into the appropriate type for the field / property
  • Throwing an appropriate exception if the value doesn't convert properly (or again, supplying a default)

Now, I wouldn't be a proper coder if I wasn't lazy, and when it comes to this kind of boring repetitive stuff, I'm well lazy. So what's the alternative?

The Solution: Declarative Parameter Binding

Well, one solution would be to just 'mark up' the fields and properties we want loaded with appropriate metadata (specifying defaults, what key to bind against and in which collection) and just let some library code perform the actual work.

Kind of like this:

[WebParameter()]
protected string FirstName;

[WebParameter("Last_Name")]
protected string LastName;

[WebParameter(IsRequired=true)]
protected int CustomerID;

The optional constructor parameter just supplies the key to find the parameter in the Request.QueryString collection, and the IsRequired property toggles whether omissions are an exception condition, rather than a skip condition.

All of a sudden, a great mass of code has condensed into a few attributes, which makes it easier to see at-a-glance what's going on, and simpler to maintain. All we need to do now is implement all that common logic we just trimmed out in some kind of helper class. This would use reflection to examine the Page class, find all the properties (or fields) we marked with the attributes, and make the relevant assignments from QueryString automatically.

(If you've never used reflection in .NET before, here's what you need to know to follow the code:

  • At runtime, every object has a Type, from which we can get a representation of the fields and properties exposed by that Type
  • These representations - FieldInfo and PropertyInfo - share a common base class - MemberInfo
  • MemberInfo subclasses provide a way of performing 'late bound' interactions with object members, rather than having to pre-specify what's going to be performed at compile-time. So a PropertyInfo allows you to do get/set'ing of that property on an object, without having to hard-code object1.property1=myValue into your source.

There're lots of tutorials about if you're interested.)

The first step is to do exactly that, just loop through the properties and members on the class (here assigned to target), and pass each member (along with the current HttpRequest (request)) down to another method that's actually going to do the work.

public static void SetValues(object target, System.Web.HttpRequest request)
{
    System.Type type =target.GetType();
    FieldInfo[] fields = 
        type.GetFields(BindingFlags.Instance | BindingFlags.Public);
    PropertyInfo[] properties = 
        type.GetProperties(BindingFlags.Instance | BindingFlags.Public);
    MemberInfo[] members =new MemberInfo[fields.Length + properties.Length];
    fields.CopyTo(members, 0);
    properties.CopyTo(members, fields.Length);

    for(int f=0;f<members.Length;f++)
        SetValue(members[f], target, request);
}

The only slightly confusing bit of this step is that we merge the properties and fields arrays into one members array, which just means we only have to do one loop round. All the real work gets done in SetValue (singular!), which we'll discuss next. Now there's a fair chunk of code here, but what it does is fairly straightforward when broken down.

Firstly, we exclude members that aren't marked with WebParameterAttribute (the outermost if), and because there's no simple way of dealing with indexed parameters, we throw an error if they've been marked (you might choose to just skip the member silently):

public static void SetValue(MemberInfo member, 
        object target, System.Web.HttpRequest request)
{
    WebParameterAttribute[] attribs;
    WebParameterAttribute attrib;
    TypeConverter converter;
    string paramValue;
    string paramName;
    object typedValue;
    bool usingDefault;

    try
    {
        attribs = (WebParameterAttribute[])
          member.GetCustomAttributes(typeof(WebParameterAttribute), true);
        if(attribs!=null && attribs.Length>0)
        {
            // Just make sure we're not going after an indexed property
            if (member.MemberType==MemberTypes.Property)
            {
                ParameterInfo[] ps =
                  ((PropertyInfo)member).GetIndexParameters();
                if (ps!=null && ps.Length>0)
                  throw new NotSupportedException("Cannot apply " + 
                  "WebParameterAttribute to indexed property");
            }

Now we get the various settings from the attribute, and the (string) parameter value itself.

// There should only be one
// WebParameterAttribute (it's a single-use attribute)
attrib =attribs[0];
paramName =(attrib.ParameterName!=null) ?
                attrib.ParameterName : member.Name;
paramValue =attrib.GetValue(paramName, request);

Note that it's the attribute itself which supplies the actual parameter value we're after, making its own determination on which part of the HttpRequest to use. Delegating responsibility for non-core activities like this makes for a more flexible solution. In our case WebParameter - the only attribute we've talked about so far - selects either Request.Form or Request.QueryString depending on the method which was used to submit the form. Alternatively other, specialized, subclasses like QueryParameter and FormParameter can perform binding to specific parts of the request, and others could be easily created for Request.Cookies or Request.Session as required.

If the attribute returns null, it signifies that the parameter wasn't found in the Request. Our options then are fairly straightforward:

  • Use the attribute's DefaultValue, if supplied
  • Throw an error if the attribute is marked IsRequired
  • Skip this member entirely by returning back to the calling method
// Handle default value assignment, if required
usingDefault =false;
if (paramValue==null)
{
    if (attrib.DefaultValue!=null)
    {
        paramValue =attrib.DefaultValue;
        usingDefault =true;
    }
    else if (!attrib.IsRequired)
        return; // Just skip the member
    else
        throw new
            ApplicationException(String.Format("Missing " +
            "required parameter '{0}'", paramName));
    }

Now (finally), we can actually take our string and assign it to the member. We've got a couple of helper methods here just to make the code simpler (I won't reproduce the full source code here, but it's all in the example files):

  • GetMemberUnderlyingType(...) returns the Type that a MemberInfo represents, i.e. returns .FieldType or .PropertyType
  • SetMemberValue(...) wraps the code required to do reflective field / property assignment

I'm using a TypeConverter to do the actual string conversion, because this delegates responsibility for the string parsing to the outside world (which is always a good idea). If you haven't come across them, the bottom line is that they're a completely extensible scheme to associate type conversion routines with the types they convert to/from. They're principally used in control development (that's how the VS.NET IDE's property browser works), but they're far too useful to only use in the designer and I've started using them all over.

C#
    // Now assign the loaded value onto the member,
    // using the relevant type converter
    // Have to perform the assignment slightly
    // differently for fields and properties
    converter =
     TypeDescriptor.GetConverter(GetMemberUnderlyingType(member));
    if (converter==null ||
      !converter.CanConvertFrom(paramValue.GetType()))
        throw new
          ApplicationException(String.Format("Could not" +
          " convert from {0}", paramValue.GetType()));

    try
    {
        typedValue =converter.ConvertFrom(paramValue);
        SetMemberValue(member, target, typedValue);
    }
    catch
    {
        // We catch errors both from the type converter
        // and from any problems in setting the field/property
        // (eg property-set rules, security, readonly properties)

        // If we're not already using the default, but there
        // is one, and we're allowed to use it for invalid data,
        // give it a go, otherwise just propagate the error

        if (!usingDefault && attrib.IsDefaultUsedForInvalid
                                    && attrib.DefaultValue!=null)
        {
          typedValue =converter.ConvertFrom(attrib.DefaultValue);
          SetMemberValue(member, target, typedValue);
        }
        else
            throw;
    }
}

Finally, we pick up on any exceptions arising from anywhere in the handling for this field, and wrap them in a standard error message

C#
    }
    catch(Exception err)
    {
        throw new ApplicationException("Property/field " +
           "{0} could not be set from request - " + err.Message, err);
    }
}

Phew.

Now the obvious place for all this code would be in a base Page class, but I have a real issue with creating unnecessary Page subclasses, so the 'engine' for my implementation is just a static method on the WebParameter class itself. I felt a bit dirty putting quite so much code on an attribute class, but it's not the end of the world.

C#
private void Page_Load(object sender, System.EventArgs e)
{
    WebParameterAttribute.SetValues(this, Request);
}

Having to call it explicitly like this also saves on the overhead when you don't need it done, and means you get to choose when to bind: first time round; postback; every request - what you choose will depend on what your page does and whether you're binding to field or viewstate property accessors.

Full Example

Having imported the relevant namespaces (and assemblies) into a page, all that's actually required to do all this is the very briefest of code (this is from the included example):

C#
public class WebParameterDemo : System.Web.UI.Page
{
    [QueryParameter("txtFirstName")]
    public string FirstName ="field default";

    [QueryParameter("txtLastName", DefaultValue="attribute default")]
    public string LastName{
        get{ return (string)ViewState["LastName"]; }
        set{ ViewState["LastName"]=value; }
    }

    [QueryParameter("txtCustomerID", DefaultValue="0")]
    public int CustomerID;

    private void Page_Load(object sender, System.EventArgs e)
    {
        WebParameterAttribute.SetValues(this, Request);
    }
}

Benefits

  • Clean, simple approach improves legibility and maintainability
  • Centralized pattern means one less thing to test at a Page level
  • Page's API is 'self documenting'
    • For paper documentation purposes
    • For other pages in the application that might want to construct requests for your page (provided they know what properties/fields they're trying to set)
    • Automated testing (e.g.: construct requests with the intention of making the page fall over)

Note also that it doesn't just have to be the Page class you bind to, any class can be 'bound', though it'd be rare for something other than a UserControl to be bound to Request.Form or Request.QueryString directly (especially since you could provide a page-level property to indirect the binding).

Other parts of Request you might want to bind to:

  • Session
  • ServerVariables
  • Headers
  • the current HttpContext.Items

Now, I was toying with using a similar approach to this for Console apps, but it turns out someone already did it (or something pretty similar), so if I've sold you on this, check out CommandLineOptions and ditch all that args[] parsing nonsense. Thank you Gert.

Drawbacks

I like this approach a lot, it gives me more time for writing the complicated convoluted bits that get me out of bed in the morning. However it has one downside, and that's performance. Like anything based on reflection, its going to be slower than the equivalent direct call; I loop through a lot of unnecessary properties looking for those that are bound (and the Page class is not exactly short on public properties); and I'm using TypeConverters to do my string-conversions, which is yet more reflection.

That being said, reflection's only a bit slower than normal. Sure you wouldn't use it in your 3D rendering routines, but compared to the cost of your database access, wire-latency and other issues inherent in a web app and it's nothing. All that ASP.NET data binding you're using - that's reflection based. Still, if you're desperate, you could probably speed it all up by:

  • Restrict the GetFields() / GetProperties() calls in WebParameterAttribute.SetValues() to only return members declared in your code behind (BindingFlags.DeclaredOnly), rather than include all the base class members (NB: Since your code behind is itself sub-classed to build the final page, this means you'd have to pass its Type into GetValues as well).
  • Provide an interface for retrieving a MemberInfo[] array of members to bind, rather than having to find them by reflection.
  • See if using TypeDescriptor.GetProperties() with an attribute filter is any quicker than 'raw' reflection.
  • Explicitly call SetValue() on a member-by-member basis in your page, rather than SetValues()
  • Provide a couple of 'baked-in' type-conversion steps before defaulting to using the TypeConverter

Finally, I should point out that in real life, I didn't hard-code the message into the exceptions like this - I stuck them in a resource file. Even though I don't intend globalizing my application, it makes it a lot easier for people re-using the assembly (including myself) to alter the messages to suit their needs without having to trawl through the source code.

History

2004-02-15 - First, much delayed, release.

Appendix 1 - Why not to subclass System.Web.UI.Page

In the 'normal' world of OO programming, people believe in building little helper objects that can be used / aggregated within other classes to perform specific bits of functionality. However, when it comes to ASP.NET, there's way too much temptation to write your handy bit of common functionality as a base page class, with the obvious drawback that to use it you've got to derive from it, which means you're not deriving from something else.

Additionally Page is a pain to subclass from - you've got properties and methods coming out of your ears and you can't make anything abstract (or have any properties that return null unless you attribute them appropriately) or the designer chokes on it. So your object hierarchy becomes a mess of virtual methods that throw exceptions along the lines of 'this method should have been overridden in a subclass'. Yuk. On top of all this, the designer slows to a crawl since it has to create all the stuff your page creates (this is especially slow if there's database access involved), and you might not get the .config file you expected for all the dependent assemblies.

My advice is to have a few base page classes, sure, but keep it simple, and aggregate as much as possible of the more complex stuff as helper objects (even if they have to get passed a reference to their containing Page).

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Australia Australia
There's some kinda mutex between money and the time to enjoy it, and it's called work.

Comments and Discussions

 
QuestionLicense? Pin
Member 1321274420-May-17 7:09
Member 1321274420-May-17 7:09 
AnswerRe: License? Pin
Member 1241352212-Jun-17 3:17
Member 1241352212-Jun-17 3:17 
GeneralMy vote of 5 Pin
thatraja5-Oct-10 23:33
professionalthatraja5-Oct-10 23:33 
GeneralTop Code Pin
Richard Birkby27-Sep-06 4:29
Richard Birkby27-Sep-06 4:29 
GeneralQuestion Pin
theBoringCoder14-Dec-05 12:07
theBoringCoder14-Dec-05 12:07 
GeneralRe: Question Pin
piers715-Dec-05 1:33
piers715-Dec-05 1:33 
Questionlimitations of Default Value Pin
epeleg21-Sep-05 23:53
epeleg21-Sep-05 23:53 
AnswerRe: limitations of Default Value Pin
piers722-Sep-05 2:26
piers722-Sep-05 2:26 
GeneralRe: limitations of Default Value Pin
epeleg22-Sep-05 10:04
epeleg22-Sep-05 10:04 
GeneralRe: limitations of Default Value Pin
epeleg22-Sep-05 10:23
epeleg22-Sep-05 10:23 
GeneralRe: limitations of Default Value Pin
piers723-Sep-05 4:58
piers723-Sep-05 4:58 
GeneralRe: limitations of Default Value Pin
epeleg23-Sep-05 22:58
epeleg23-Sep-05 22:58 
GeneralExcellent Pin
Hugo Hallman20-Jan-05 0:43
Hugo Hallman20-Jan-05 0:43 
GeneralInteresting, but... Pin
worldspawn14-Apr-04 16:32
worldspawn14-Apr-04 16:32 
Questionuse in custom controls? Pin
Yannick Smits14-Apr-04 8:13
Yannick Smits14-Apr-04 8:13 
AnswerRe: use in custom controls? Pin
piers713-Jan-05 0:30
piers713-Jan-05 0:30 
GeneralAlong the same lines Pin
boogs5-Mar-04 5:38
boogs5-Mar-04 5:38 
GeneralRe: Along the same lines Pin
Anonymous8-Aug-05 5:12
Anonymous8-Aug-05 5:12 
GeneralRe: Along the same lines Pin
piers722-Sep-05 2:08
piers722-Sep-05 2:08 
GeneralNice Stuff... Pin
MR_SAM_PIPER4-Mar-04 13:09
MR_SAM_PIPER4-Mar-04 13:09 
GeneralDagnamit Pin
Christian Graus4-Mar-04 12:02
protectorChristian Graus4-Mar-04 12:02 
I've just recently learned about doing this and I thought it might be time for me to write another article, but you beat me to it. I'll be reading over it though, just to make sure I fully understand this approach, and in case you know something I don't. Smile | :)

Christian

I have drunk the cool-aid and found it wan and bitter. - Chris Maunder
GeneralGreat article Pin
Jeffry van de Vuurst4-Mar-04 11:48
Jeffry van de Vuurst4-Mar-04 11:48 
GeneralExcellent Pin
Andy Brummer4-Mar-04 7:56
sitebuilderAndy Brummer4-Mar-04 7:56 
GeneralRe: Excellent Pin
Ian Lane24-Mar-04 9:58
Ian Lane24-Mar-04 9:58 
GeneralRe: Excellent Pin
piers713-Jan-05 3:08
piers713-Jan-05 3:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.