Click here to Skip to main content
15,881,248 members
Articles / Web Development / ASP.NET

How to use a custom expression builder to provide declarative, strongly-typed references to types and members

Rate me:
Please Sign up or sign in to vote.
4.95/5 (18 votes)
3 Feb 2009CPOL8 min read 58.4K   451   40   4
This article provides an example of how to implement and use a custom expression builder to extend compile-time support in your ASP.NET pages.

Introduction

ASP.NET 2.0 introduced a new form of declarative expression syntax that can be used when specifying values for control properties in page markup. Most ASP.NET developers will have seen or written code like this before:

ASP.NET
<asp:Label Text="<%$ Resources: Headings, CustomerDetailsPage %>" />

<asp:Literal Text="<%$ AppSettings: SiteName %>" />

Declarative expressions such as this are parsed at compile-time, and the resulting code expression is evaluated and bound to the target property at run-time. The prefix of the expression maps to a known expression builder which does all the work of parsing and evaluating the expression, which is the segment after the colon. There are three built-in expression builders in ASP.NET 2.0:

  • AppSettingsExpressionBuilder: allows a reference to a setting in the AppSettings configuration to be obtained at runtime, using the setting's key.
  • ConnectionStringsExpressionBuilder: allows a reference to a setting in the ConnectionStrings configuration to be obtained at run-time, using the setting's key.
  • ResourceExpressionBuilder: evaluates at run-time to a global or local resource string for the current culture, using the resource's key and class name.

Of these, the ResourceExpressionBuilder is perhaps the most commonly used, as it provides easy, declarative localization of server controls, with no extra code or data-binding required.

One of the hidden advantages of an expression builder is that it can be used to execute custom code at compile-time, providing a hook into the page compilation process which can be used for performing tasks such as Reflection, file validation, URL validation, etc. Providing compile-time support is useful in scenarios where we want to try and eliminate errors that could otherwise only be caught at run-time.

As with virtually any other feature in ASP.NET 2.0, you can extend the expression builder framework with your own custom expression builders as well, and this is where we can really start to use the power of expressions.

Tutorial

Let's start with a known problem in ASP.NET, and see how we can develop a custom expression builder as a solution. There are a few controls that define the properties to refer to type names and member names, most notably the ObjectDataSource control, which will be used as the example for the rest of the article. These properties are specified as strings, either in the markup or in the code-behind, and the control resolves the type and member information at runtime, throwing an error if the type information is incorrect.

If we define the type and member names as raw strings in the markup or code-behind, then we gain no support from the compiler, and errors will not be caught until the page is executed. Of course, we could just use a static reference to the type in the code-behind:

C#
myDataSource.TypeName = typeof(MyComponents.MyBusinessObject).FullName;

This would effectively give us compile-time validation of the type itself without using Reflection, provided the type was already known to the compiler. This method cannot validate members however, and it mandates the use of code-behind.

The ideal solution would be declarative, and provide compile-time validation of both types and members within the IDE and the target application compilation context - fortunately, this is exactly what a custom expression builder can provide.

First, we define the expression syntax that we want to work with, the expected behaviour of the expression when it is parsed and evaluated, and the return value. Expressions are always in the form:

<%$ [prefix]: [expression] %>

where [prefix] is a keyword that maps to an available expression builder, and [expression] is the raw string data that will be evaluated by the target builder. The expression data can be any valid string, in any format we choose. For this example, we will use the following syntax:

<%$ Reflect: TypeName[, MemberName] %>

where TypeName is mandatory, and must be the full-name of the type, and MemberName is the name of the member. MemberName is optional. If we only provide the type name, then the expression returns the type name as a string; otherwise, if the member name is provided, then that is returned instead. This means, we would expect our markup to look like this:

ASP.NET
<asp:ObjectDataSource ID="MySource" Runat="server"
    TypeName="<%$ Reflect: MyComponents.MyBusinessObject %>" 
    SelectMethod="<%$ Reflect: MyComponents.MyBusinessObject, GetSearchResults %>"
    ... />

which results in the following markup after the expressions are evaluated:

ASP.NET
<asp:ObjectDataSource ID="MySource" Runat="server"
    TypeName="MyComponents.MyBusinessObject" 
    SelectMethod="GetSearchResults"
    ... />

The choice of prefix is entirely arbitrary - I've just gone for something that most closely identifies the function being performed by the expression builder, but you could choose anything you want in your own implementation. Also, we only expect public types and members to be resolved in this manner, and we should ensure that type resolution is case-sensitive. If either the type or member name cannot be resolved, then we want to see an exception at compile-time, or at execution-time if the page is not compiled.

Let's implement the class for our custom expression builder first:

C#
using System;
using System.CodeDom;
using System.Linq;
using System.Security.Permissions;
using System.Web;
using System.Web.Compilation;
using System.Web.UI;

namespace CustomExpressionBuilderSample
{
    [ExpressionPrefix("Reflect")]
    [AspNetHostingPermission(SecurityAction.InheritanceDemand, 
                             Level=AspNetHostingPermissionLevel.Minimal)]
    [AspNetHostingPermission(SecurityAction.LinkDemand, 
                             Level=AspNetHostingPermissionLevel.Minimal)]
    public class ReflectExpressionBuilder : ExpressionBuilder    
    {    

    }
}

The main requirement is that we inherit from ExpressionBuilder, which is defined in the System.Web.Compilation namespace in the System.Web assembly. We also decorate the class with an ExpressionPrefixAttribute, passing the prefix we use to map to the expression builder, which gives us useful design-time support for these expressions. I've added the hosting attributes to flesh out the builder, but they are not required.

The next step is to decide whether we need to parse the expression before it is evaluated. In this case, we are going to parse the expression to ensure that it matches the format we expect, and to perform type and member validation as well. Whenever the page is compiled, an expression builder is created for the expression prefix, and the ParseExpression method is called on the builder, which just passes through the raw expression unless we override this method and provide our own parsing logic.

Let's add the ParseExpression implementation, which uses a custom method, ValidateExpression, to validate the type information:

C#
/// <summary>
/// Parses and validates the expression data and returns a canonical type or member name, 
/// or throws an exception if the expression is invalid.
/// </summary>
/// <param name="expression">The raw expression to parse.</param>
/// <param name="propertyType">The target property type.</param>
/// <param name="context">Contextual information for the expression builder.</param>
/// <returns>A string representing the target type or member name for binding.</returns>
public override object ParseExpression(string expression, 
                Type propertyType, ExpressionBuilderContext context)
{
    bool parsed = false;
    string typeName = null;
    string memberName = null;
    
    if (!String.IsNullOrEmpty(expression))
    {
        var parts = expression.Split(',');
        if (parts.Length > 0 && parts.Length < 3)
        {
            if (parts.Length == 1)
            {
                typeName = parts[0].Trim();
            }
            else if (parts.Length == 2)
            {
                typeName = parts[0].Trim();
                memberName = parts[1].Trim();
            }
        
            parsed = true;
        }
    }
    
    if (!parsed)
    {
        throw new HttpException(String.Format("Invalid Reflect" + 
                  " expression - '{0}'.", expression));
    }
    
    // now validate the expression fields
    return ValidateExpression(typeName, memberName);
}

/// <summary>
/// Validates that the specified type and member name
/// can be resolved in the current context.
/// Member name resolution is optional.
/// </summary>
/// <param name="typeName">The full name of the type.</param>
/// <param name="memberName">The member name to resolve, or null to ignore.</param>
/// <returns>The type or member name as a string
///        for binding to the target property.</returns>
private string ValidateExpression(string typeName, string memberName)
{
    // resolve type name first
    Type resolvedType = null;
    foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
    {
        resolvedType = assembly.GetType(typeName, false, false);
        if (resolvedType != null)
        {
            break;
        }
    }
    
    // if type was not resolved then raise error
    if (resolvedType == null)
    {
        var message = String.Format("Reflect Expression: Type '{0}' could " + 
                      "not be resolved in the current context.", typeName);
        throw new HttpCompileException(message);
    }
    
    // resolve the member name if provided - don't care about multiple matches
    string bindingValue = typeName;
    if (!String.IsNullOrEmpty(memberName))
    {
        bindingValue = memberName;
        if (!resolvedType.GetMember(memberName).Any())
        {
            var message = String.Format("Reflect Expression: Member '{0}' " + 
                          "'for type '{1}' does not exist.", memberName, typeName);
            throw new HttpCompileException(message);    
        }
    }
    
    return bindingValue;
}

With the addition of these two methods, we have actually done most of the work required - ParseExpression ensures that the expression declaration is valid, while ValidateExpression makes sure that the type and member arguments can be resolved, and returns the appropriate binding value.

Next, we need to provide a way for the expression builder to be evaluated at compile-time. Once ParseExpression returns the expression value, the GetCodeExpression method will be called, which needs to return a CodeDom element which can be included in the compilation tree for the page, which, when executed, renders the value of the expression. A discussion of CodeDom by itself is well beyond the scope of the article, but fortunately, we only require an extremely simple code expression for the purposes of this example, due to the fact that we do not need to perform any run-time evaluation once the expression has been parsed and validated.

The GetCodeExpression method looks like this:

C#
/// <summary>
/// Returns a CodeDom expression for invoking the expression from a compiled page at runtime.
/// </summary>
/// <param name="entry">The entry for the bound property.</param>
/// <param name="parsedData">The parsed expression data.</param>
/// <param name="context">The expression builder context.</param>
/// <returns>A <see cref="CodeExpression" /> for invoking the expression.</returns>
public override CodeExpression GetCodeExpression(BoundPropertyEntry entry, 
                object parsedData, ExpressionBuilderContext context)
{
    return new CodePrimitiveExpression((string) parsedData);
}

Very simple indeed. The parsedData parameter is the result from the call to ParseExpression, which in our case is the type name or member name as a string, so all we need to do is include it as a primitive literal in the output compilation tree. We use a new instance of the CodePrimitiveExpression class to achieve this.

As it stands, our implementation is complete, and we could now use this expression builder in our web applications. However, for completeness, we should also support the scenario where the page is not compiled at all, and the expression is parsed, validated, and evaluated at run-time. This would not be the most valuable usage of our expression builder, but for the purposes of this article, we will cover it off anyway.

To support run-time evaluation, we need to override a property on the base class, and override the EvaluateExpression method, as follows:

C#
/// <summary>
/// Gets a flag that indicates whether the expression builder
/// supports no-compile evaluation.
/// Returns true, as the target type can be validated at runtime as well.
/// </summary>
public override bool SupportsEvaluate
{
    get { return true; }
}

/// <summary>
/// Evaluates the expression at runtime.
/// </summary>
/// <param name="target">The target object.</param>
/// <param name="entry">The entry for the property bound to the expression.</param>
/// <param name="parsedData">The parsed expression data.</param>
/// <param name="context">The current expression builder context.</param>
/// <returns>A string representing the target type or member.</returns>
public override object EvaluateExpression(object target, BoundPropertyEntry entry, 
                object parsedData, ExpressionBuilderContext context)
{
    return (string) parsedData;
}

As we are using Reflection to validate the types and members, we can provide run-time support inherently. We override the SupportsEvaluate property to return true (false is the default value), and provide an implementation of EvaluateExpression, which in this case only needs to return a string cast of the parsed data.

That completes the implementation of our custom expression builder - the final steps are to wire the builder into our I file, and start writing expressions that use it. The builder can be included in your configuration as follows:

XML
<system.web>
    <compilation>
        <expressionBuilders>
            <add expressionPrefix="Reflect" 
               type="CustomExpressionBuilderSample.ReflectExpressionBuilder, 
                     CustomExpressionBuilderSample" />
        </expressionBuilders>
    </compilation>
</system.web>

Now, you can start adding strongly-typed references to types and members on any controls that expect type and member names, and know that any related errors will be caught when you compile the page. A trick I use for my web application projects is to invoke the ASP.NET compiler from my build scripts as a post-build action, so that all the pages in the site are compiled as well:

C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_compiler.exe 
     -m /LM/W3SVC/1/ROOT -f PrecompiledOutput

This works nicely with any Reflect expressions, and can help to prevent errors that might otherwise creep into production unnoticed.

Further Investigation

There are a number of other methods that can be overridden by a custom expression builder, but I have not seen any practical usage of them. It is also possible to extend the design-time support for custom expressions by adding an ExpressionDesignerAttribute to the expression builder, which refers to a type that provides a design-time UI for composing an expression value. In the case of this example, you could create a control that allows a user to search for a type in a list of assemblies, or a specific member defined on a type. You can investigate the existing expression designers in System.Web.UI.Design using Reflector, for more information on implementation.

The format of return values was largely inferred from working with the ObjectDataSource control, but you can always change the expression syntax to support more formats for the return value, dependent on your needs.

I would also recommend looking at the System.CodeDom namespace, and using Reflector to examine the built-in implementations of AppSettingsExpressionBuilder, ResourceExpressionBuilder, and ConnectionStringsExpressionBuilder, to get more ideas on how to implement your own custom expression builder.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Octet Finance
Australia Australia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralNice extension Pin
craigd7-Sep-09 19:49
craigd7-Sep-09 19:49 
GeneralMy vote of 1 Pin
albert_cook11-Aug-09 21:36
albert_cook11-Aug-09 21:36 
GeneralRe: My vote of 1 Pin
Jamie Nordmeyer9-Dec-09 3:56
Jamie Nordmeyer9-Dec-09 3:56 
GeneralRe: My vote of 1 Pin
MR_SAM_PIPER9-Dec-09 11:44
MR_SAM_PIPER9-Dec-09 11:44 

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.