Click here to Skip to main content
15,886,513 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  
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.
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
{
	/// <summary>
	/// Implements an expression builder that provides a strongly-typed reference to a type or member info, instead of a string.
	/// Targetted properties still receive a string as input, but the expression is validated during page compilation or execution.
	/// If the supplied expression cannot be resolved to a type or member info from an assembly loaded into the current domain
	/// then an exception is thrown. Note that type and member name resolution is case-sensitive. 
	/// Only public types and members can be resolved.
	/// </summary>
	/// <remarks>
	/// The following examples illustrate the usage of the expression in web pages:
	/// 
	/// &lt;asp:ObjectDataSource SelectMethod="&lt;%$ Reflect: ComCard.Components.BusinessObjects.Task, Search %>" ... />
	/// 
	/// This will resolve the type ComCard.Components.BusinessObjects.Task, and validate that Search is a public member on that type.
	/// The member name will be returned for binding to the property.
	/// 
	/// &lt;asp:ObjectDataSource DataObjectTypeName="&lt;%$ Reflect: ComCard.Components.BusinessObjects.LogEntry %>" ... />
	/// 
	/// This will resolve the type ComCard.Components.BusinessObjects.LogEntry. The type name will be returned for binding.
	/// </remarks>
	[ExpressionPrefix("Reflect")]
	[AspNetHostingPermission(SecurityAction.InheritanceDemand, Level=AspNetHostingPermissionLevel.Minimal)]
	[AspNetHostingPermission(SecurityAction.LinkDemand, Level=AspNetHostingPermissionLevel.Minimal)]
	public class ReflectExpressionBuilder : ExpressionBuilder
	{
		#region Properties
		/// <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; }
		}
		#endregion
		
		#region Public Methods
		/// <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;
		}

		/// <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>
		/// 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);
		}
		#endregion
		
		#region Private Methods
		/// <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 / 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;
		}
		#endregion
	}
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

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