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 express开发者_StackOverflow社区ion (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.
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."
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;
}
精彩评论