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
    }
}

2 comments:

  1. Kingfisher We appreciate having access to deskpins because it provides a reliable method for pinning windows conveniently The software remains efficient performance is smooth and functionality is valuable It supports users in maintaining focus while managing multiple active tasks.

    ReplyDelete
  2. Meadow Thank you for maintaining this mousecape utility that consistently delivers useful customization functionality with ease The application remains lightweight operation is smooth and usability is impressive It creates a more personalized desktop experience through thoughtful and effective features.

    ReplyDelete