Tuesday, October 4, 2011

Dynamic expression for LINQ

Here is a class I wrote a while ago for building dynamic lambda expression to be used against a LINQ to SQL data context. Yes, it's a giant beast that shows I didn't have any refactoring skills at some point in time, but the intent of the class is good and will hopefully help out someone.
using System;
using System.Linq.Expressions;

namespace DataAccess.DomainModel.Entities.BaseEntities
{
    public abstract class BaseSearchClass<T>
    {
        #region Variables

        private SearchClauseSeparator _clauseSeparator = SearchClauseSeparator.Or;

        #endregion

        #region Properties

        public SearchClauseSeparator ClauseSeparator
        {
            get { return _clauseSeparator; }
            set { _clauseSeparator = value; }
        }

        #endregion

        #region Expression Building Methods

        public virtual Expression<Func<T, bool>> BuildExpression()
        {
            // Create return object
            var param = Expression.Parameter(typeof(T), "x");
            Expression workingExpression = null;

            // Loop through all properties
            foreach (var property in this.GetType().GetProperties())
            {
                // Skip if missing SearchProperty attribute
                var attrs = property.GetCustomAttributes(typeof(SearchableProperty), false);
                if (attrs == null || attrs.Length == 0) continue;

                // Get the attribute properties
                var attr = attrs[0] as SearchableProperty;
                var searchOperation = attr.SearchOperation;
                var dataFieldName = attr.DataFieldName;
                if (string.IsNullOrEmpty(dataFieldName)) dataFieldName = property.Name;
                var prependOperator = attr.PrependOperator;
                var expressionBuildingDelegate = attr.ExpressionBuildingDelegate;

                // Get the value
                var value = property.GetValue(this, null);
                if (value == null) continue;

                // Build the expression
                var exp = GenerateExpressionForProperty(param, searchOperation, dataFieldName, value, expressionBuildingDelegate);
                if (exp == null) continue;

                // Combine expressions accordingly
                if ((_clauseSeparator == SearchClauseSeparator.Or && prependOperator == SearchClauseSeparator.None) || prependOperator == SearchClauseSeparator.Or)
                {
                    workingExpression = OrCombineExpressions(workingExpression, exp);
                }
                else
                {
                    workingExpression = AndCombineExpressions(workingExpression, exp);
                }
            }

            // See if expression is null
            if (workingExpression == null)
            {
                workingExpression = CreateDefaultExpression(param);
            }

            // Return the lambda of the expression
            return Expression.Lambda<Func<T, bool>>(workingExpression, param);
        }

        private Expression GenerateExpressionForProperty(Expression parameter, SearchOperation searchOperation,
            string propertyName, object value, ExpressionDelegate expressionDelegate)
        {
            // Stop if value is null
            if (value == null) return null;

            // See if field is nullable
            var propertyType = typeof(T).GetProperty(propertyName).PropertyType;
            var isNullable = propertyType.Name.StartsWith("Nullable");

            // See which operation to perform
            var operation = expressionDelegate;
            if (expressionDelegate == null) operation = DetermineOperator(searchOperation);

            // See if value is an array
            if (value.GetType().IsArray)
            {
                Expression returnExpression = null;
                var theArray = value as Array;
                foreach (var item in theArray)
                {
                    returnExpression =
                        OrCombineExpressions(returnExpression,
                                             BuildSingleExpression(parameter, propertyName, propertyType,
                                                                   item, operation, isNullable));
                }
                return returnExpression;
            }
            
            // See if value is a DualValuedObject (for performing BETWEEN operations)
            if (value is DualValuedObject)
            {
                // Stop if either value is null
                var dvo = value as DualValuedObject;
                if (dvo.Value1 == null || dvo.Value2 == null) return null;

                // Build left side
                var left = BuildSingleExpression(parameter, propertyName, propertyType,
                    dvo.Value1, Expression.GreaterThanOrEqual, isNullable);

                // Build right side
                var right = BuildSingleExpression(parameter, propertyName, propertyType,
                    dvo.Value2, Expression.LessThanOrEqual, isNullable);
                
                // Combine them using AND
                return AndCombineExpressions(left, right);
            }

            // Return a single expression
            return BuildSingleExpression(parameter, propertyName, propertyType, value, operation, isNullable);
        }

        private Expression BuildSingleExpression(Expression parameter, string propertyName, Type propertyType,
            object value, ExpressionDelegate delegateFunction, bool isNullable)
        {
            // Throw exception if property and value or not of the same type
            if (!propertyType.ToString().Contains(value.GetType().ToString()))
            {
                throw new Exception(propertyName + " is a " + propertyType.ToString() +
                    ", invalid attempt to compare to a " + value.GetType().ToString());
            }

            // Create the left and right hands of the expression
            var left = Expression.Property(parameter, propertyName);
            if (isNullable) left = Expression.Property(left, "Value");
            var right = Expression.Constant(value);

            // Combine using the specified operation
            return delegateFunction(left, right);
        }

        private Expression OrCombineExpressions(Expression left, Expression right)
        {
            return (left == null) ? right : Expression.Or(left, right);
        }

        private Expression AndCombineExpressions(Expression left, Expression right)
        {
            return (left == null) ? right : Expression.And(left, right);
        }

        private Expression CreateDefaultExpression(Expression parameter)
        {
            return BuildSingleExpression(parameter, "ID", typeof(int), 0, Expression.GreaterThan, false);
        }

        #endregion

        #region SearchOperation Methods

        private ExpressionDelegate DetermineOperator(SearchOperation type)
        {
            switch (type)
            {
                case SearchOperation.GreaterThanOrEqualTo :
                    return Expression.GreaterThanOrEqual;
                case SearchOperation.GreaterThan :
                    return Expression.GreaterThan;
                case SearchOperation.LessThanOrEqualTo :
                    return Expression.LessThanOrEqual;
                case SearchOperation.LessThan :
                    return Expression.LessThan;
                case SearchOperation.Contains :
                    return Contains;
                case SearchOperation.StartsWith :
                    return StartsWith;
                case SearchOperation.EndsWith :
                    return EndsWith;
                case SearchOperation.Equal :
                default:
                    return Expression.Equal;
            }
        }

        private static Expression CustomStringEvaluator(string stringEvaluator, Expression property, ConstantExpression value)
        {
            var method = typeof(string).GetMethod(stringEvaluator, new[] { typeof(string) });
            return Expression.Call(property, method, value);
        }

        private static Expression Contains(Expression property, ConstantExpression value)
        {
            return CustomStringEvaluator("Contains", property, value);
        }

        private static Expression StartsWith(Expression property, ConstantExpression value)
        {
            return CustomStringEvaluator("StartsWith", property, value);
        }

        private static Expression EndsWith(Expression property, ConstantExpression value)
        {
            return CustomStringEvaluator("EndsWith", property, value);
        }

        #endregion
    }
}

No comments:

Post a Comment