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: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:
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: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: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:
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:
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));
}
return ValidateExpression(typeName, memberName);
}
private string ValidateExpression(string typeName, string memberName)
{
Type resolvedType = null;
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
resolvedType = assembly.GetType(typeName, false, false);
if (resolvedType != null)
{
break;
}
}
if (resolvedType == null)
{
var message = String.Format("Reflect Expression: Type '{0}' could " +
"not be resolved in the current context.", typeName);
throw new HttpCompileException(message);
}
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:
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:
public override bool SupportsEvaluate
{
get { return true; }
}
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:
<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.