Generic method for constructing a string Dynamic Linq query. includes strings that have been tokenized

dynamic-linq linq

Question

I am using Expression.And and Expression.Or to build dynamic linq queries. When the property/field being queried is a string, and the string contains spaces I would like to tokenize the string on the spaces and create an "And'd" sub query on the tokens.

Here is what I mean in a non generic fashion

var tokens = Code.Split(new []{" "}, StringSplitOptions.RemoveEmptyEntries);
var index = 0;

var firstToken = tokens[index ++];
Expression<Func<Entity, bool>> subQuery =
                                 entity => entity.Code.Contains(firstToken);

for (; index < tokens.Length; index ++)
{
    var tempToken = tokens[index];
    subQuery = subQuery.And(entity => entity.Code.Contains(tempToken));
}

query = query.Or(subQuery);

What I'd like to do is find a way of writing a method which is generic enough to just call for example:

PredicateBuilder.BuildTokenizedStringQuery<Entity>(
                                   tokens, entity => entity.Code);

and I end up with the same result. The following is where I'm at but I can't use the Func stringProp accessor in and Expression. I have to somehow combine an accessor expression (of the string property) with an invocation expression (that invokes string.Contains)

private Expression<Func<T, bool>> BuildTokenizedStringQuery<T>(string[] tokens,
                                                    Func<T, string> stringProp)
{
    var index = 0;
    var firstToken = tokens[index++];
    Expression<Func<T, bool>> subQuery = entity => 
                                       stringProp(entity).Contains(firstToken);
    for (; index < tokens.Length; index++)
    {
        var tempToken = tokens[index];
        subQuery = subQuery.And(
                             entity => stringProp(entity).Contains(tempToken));
    }

    return subQuery;
}

I'd also be interested to hear if this all looks like a bad idea.

1
1
5/6/2011 11:20:44 AM

Accepted Answer

The answer Josh provided is awesome and helped me to get exactly what I want. It however tests for equality of each token (it is also more generic as equality can be tested against any type) as opposed to a string.Contains test. Here is a solution that gives a string.Contains result:

public static Expression<Func<T, bool>>
           BuildTokenizedStringQuery<T>(string[] tokens,
                             Expression<Func<T, string>> stringPropertyAccessor)
{
    ParameterExpression parameterExpression = stringPropertyAccessor.Parameters
                                                                    .Single();

    var index = 0;
    var firstToken = tokens[index ++];

    Expression<Func<string, bool>> contains =
                                      aString => aString.Contains(firstToken);
    var invocation = Expression.Invoke(contains, stringPropertyAccessor.Body);

    Expression<Func<T, bool>> expression = Expression
                         .Lambda<Func<T, bool>>(invocation, parameterExpression);

    for (; index < tokens.Length; index++)
    {
        var tempToken = tokens[index];

        contains = aString => aString.Contains(tempToken);
        invocation = Expression.Invoke(contains, stringPropertyAccessor.Body);

        expression = expression.And(Expression
                        .Lambda<Func<T, bool>>(invocation, parameterExpression));
    }

    return expression;
}
0
5/6/2011 2:58:55 AM

Popular Answer

Here's what I use to do this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using System.Collections.ObjectModel;

namespace MyLibrary.Extensions
{
    /// <summary>Defines extension methods for building and working with Expressions.</summary>
    public static class ExpressionExtensions
    {
        /// <summary>Ands the Expressions.</summary>
        /// <typeparam name="T">The target type of the Expression.</typeparam>
        /// <param name="expressions">The Expression(s) to and.</param>
        /// <returns>A new Expression.</returns>
        public static Expression<Func<T, bool>> And<T>(this IEnumerable<Expression<Func<T, bool>>> expressions)
        {
            if (expressions.IsNullOrEmpty())
                return null;

            Expression<Func<T, bool>> finalExpression = expressions.First();

            foreach (Expression<Func<T, bool>> e in expressions.Skip(1))
                finalExpression = finalExpression.And(e);

            return finalExpression;
        }

        /// <summary>Ors the Expressions.</summary>
        /// <typeparam name="T">The target type of the Expression.</typeparam>
        /// <param name="expressions">The Expression(s) to or.</param>
        /// <returns>A new Expression.</returns>
        public static Expression<Func<T, bool>> Or<T>(this IEnumerable<Expression<Func<T, bool>>> expressions)
        {
            if (expressions.IsNullOrEmpty())
                return null;

            Expression<Func<T, bool>> finalExpression = expressions.First();

            foreach (Expression<Func<T, bool>> e in expressions.Skip(1))
                finalExpression = finalExpression.Or(e);

            return finalExpression;
        }

        /// <summary>Ands the Expression with the provided Expression.</summary>
        /// <typeparam name="T">The target type of the Expression.</typeparam>
        /// <param name="expression1">The left Expression to and.</param>
        /// <param name="expression2">The right Expression to and.</param>
        /// <returns>A new Expression.</returns>
        public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
        {
            //Reuse the first expression's parameter
            ParameterExpression param = expression1.Parameters.Single();
            Expression left = expression1.Body;
            Expression right = RebindParameter(expression2.Body, expression2.Parameters.Single(), param);
            BinaryExpression body = Expression.AndAlso(left, right);

            return Expression.Lambda<Func<T, bool>>(body, param);
        }

        /// <summary>Ors the Expression with the provided Expression.</summary>
        /// <typeparam name="T">The target type of the Expression.</typeparam>
        /// <param name="expression1">The left Expression to or.</param>
        /// <param name="expression2">The right Expression to or.</param>
        /// <returns>A new Expression.</returns>
        public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
        {
            //Reuse the first expression's parameter
            ParameterExpression param = expression1.Parameters.Single();
            Expression left = expression1.Body;
            Expression right = RebindParameter(expression2.Body, expression2.Parameters.Single(), param);
            BinaryExpression body = Expression.OrElse(left, right);

            return Expression.Lambda<Func<T, bool>>(body, param);
        }

        /// <summary>Updates the supplied expression using the appropriate parameter.</summary>
        /// <param name="expression">The expression to update.</param>
        /// <param name="oldParameter">The original parameter of the expression.</param>
        /// <param name="newParameter">The target parameter of the expression.</param>
        /// <returns>The updated expression.</returns>
        private static Expression RebindParameter(Expression expression, ParameterExpression oldParameter, ParameterExpression newParameter)
        {
            if (expression == null)
                return null;

            switch (expression.NodeType)
            {
                case ExpressionType.Parameter:
                {
                    ParameterExpression parameterExpression = (ParameterExpression)expression;

                    return (parameterExpression.Name == oldParameter.Name ? newParameter : parameterExpression);
                }
                case ExpressionType.MemberAccess:
                {
                    MemberExpression memberExpression = (MemberExpression)expression;

                    return memberExpression.Update(RebindParameter(memberExpression.Expression, oldParameter, newParameter));
                }
                case ExpressionType.AndAlso:
                case ExpressionType.OrElse:
                case ExpressionType.Equal:
                case ExpressionType.NotEqual:
                case ExpressionType.LessThan:
                case ExpressionType.LessThanOrEqual:
                case ExpressionType.GreaterThan:
                case ExpressionType.GreaterThanOrEqual:
                {
                    BinaryExpression binaryExpression = (BinaryExpression)expression;

                    return binaryExpression.Update(RebindParameter(binaryExpression.Left, oldParameter, newParameter), binaryExpression.Conversion, RebindParameter(binaryExpression.Right, oldParameter, newParameter));
                }
                case ExpressionType.Call:
                {
                    MethodCallExpression methodCallExpression = (MethodCallExpression)expression;

                    return methodCallExpression.Update(RebindParameter(methodCallExpression.Object, oldParameter, newParameter), methodCallExpression.Arguments.Select(arg => RebindParameter(arg, oldParameter, newParameter)));
                }
                case ExpressionType.Invoke:
                {
                    InvocationExpression invocationExpression = (InvocationExpression)expression;

                    return invocationExpression.Update(RebindParameter(invocationExpression.Expression, oldParameter, newParameter), invocationExpression.Arguments.Select(arg => RebindParameter(arg, oldParameter, newParameter)));
                }
                default:
                {
                    return expression;
                }
            }
        }

        public static Expression<Func<T, bool>> BuildContainsExpression<T, R>(Expression<Func<T, R>> valueSelector, IEnumerable<R> values)
        {
            if (null == valueSelector)
                throw new ArgumentNullException("valueSelector");

            if (null == values)
                throw new ArgumentNullException("values");

            ParameterExpression parameterExpression = valueSelector.Parameters.Single();
            IEnumerable<BinaryExpression> equalExpressions = null;
            Expression aggregationExpression = null;

            if (!values.IsNullOrEmpty())
                return (e => false);

            equalExpressions = values.Select(v => Expression.Equal(valueSelector.Body, Expression.Constant(v, typeof(R))));
            aggregationExpression = equalExpressions.Aggregate<Expression>((accumulate, equal) => Expression.Or(accumulate, equal));

            return Expression.Lambda<Func<T, bool>>(aggregationExpression, parameterExpression);
        }

        public static Expression<Func<T, bool>> BuildDoesNotContainExpression<T, R>(Expression<Func<T, R>> valueSelector, IEnumerable<R> values)
        {
            if (null == valueSelector)
                throw new ArgumentNullException("valueSelector");

            ParameterExpression parameterExpression = valueSelector.Parameters.Single();
            IEnumerable<BinaryExpression> notEqualExpressions = null;
            Expression aggregationExpression = null;

            if (!values.IsNullOrEmpty())
                return (e => false);

            notEqualExpressions = values.Select(v => Expression.NotEqual(valueSelector.Body, Expression.Constant(v, typeof(R))));
            aggregationExpression = notEqualExpressions.Aggregate<Expression>((accumulate, equal) => Expression.And(accumulate, equal));

            return Expression.Lambda<Func<T, bool>>(aggregationExpression, parameterExpression);
        }
    }
}

Usage

string query = "kill mockingbird";
string[] tokens = query.Split(' ');
Expression<Func<Book, string>> inClause = BuildContainsExpression<Book, string>(o => o.Title, tokens);

using (LibraryDataContext dataContext = new LibraryDataContext())
{
    List<Book> matchingBooks = dataContext.Books.Where(inClause).ToList();
}

Results

This will find all books whose title contains the words "kill" or "mockingbird."



Related Questions





Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow