Reputation: 21420
I have the following method which builds an expression based on a column name and value:
public Func<TSource, bool> SimpleComparison<TSource>(string property, object value)
{
var type = typeof (TSource);
var pe = Expression.Parameter(type, "p");
var propertyReference = Expression.Property(pe, property);
var constantReference = Expression.Constant(value);
return Expression.Lambda<Func<TSource, bool>>
(Expression.Equal(propertyReference, constantReference),
new[] { pe }).Compile();
}
I am trying to create a list of expressions, then query my datasource with the final concatenated/compiled expression.
I have tried to use Expression.Block
but the concept is a bit difficult for me to grasp. I have also seen Expression.Loop
but am not sure if it's what I need to use here.
Ideally, I would be able to do something like this:
var filters = request.Filter.Filters;
IQueryable<MyDTO> myDataSource = context.MyDataSource;
var expressions = null;
foreach (var filter in filters)
{
expressions.Add(SimpleExpression<MyDTO>(filter.Name, filter.Value));
}
return myDataSource.Where(expressions);
Any ideas on how to do something like this?
Upvotes: 1
Views: 1409
Reputation: 31
I faced your exact problem recently and the below solution worked for me when trying to combine multiple expressions with either && or ||:
public Expression<Func<TSource, bool>> SimpleComparison<TSource>(List<QueryFilterObject> queryFilterObjects)
{
//initialize the body expression
BinaryExpression bodyExpression = null;
BinaryExpression andExpressionBody = null;
BinaryExpression orExpressionBody = null;
//create parameter expression
ParameterExpression parameterExpression = Expression.Parameter(typeof(TSource), "DynamicFilterQuery");
//list of binary expressions to store either the || or && operators
List<BinaryExpression> andExpressions = new List<BinaryExpression>();
List<BinaryExpression> orExpressions = new List<BinaryExpression>();
foreach (var queryFilterObject in queryFilterObjects)
{
//create member property expression
var property = Expression.Property(parameterExpression, queryFilterObject.PropertyName);
//create the constant expression value
var constantExpressionValue = Expression.Constant(queryFilterObject.Value, queryFilterObject.PropertyType);
//create the binary expression clause based on the comparison operator
BinaryExpression clause = null;
if (queryFilterObject.ComparisonOperator == "==")
{
clause = Expression.Equal(property, constantExpressionValue);
}
else if (queryFilterObject.ComparisonOperator == "!=")
{
clause = Expression.NotEqual(property, constantExpressionValue);
}
else if (queryFilterObject.ComparisonOperator == ">")
{
clause = Expression.GreaterThan(property, constantExpressionValue);
}
else if (queryFilterObject.ComparisonOperator == ">=")
{
clause = Expression.GreaterThan(property, constantExpressionValue);
}
else if (queryFilterObject.ComparisonOperator == "<")
{
clause = Expression.LessThan(property, constantExpressionValue);
}
else if (queryFilterObject.ComparisonOperator == "<=")
{
clause = Expression.LessThanOrEqual(property, constantExpressionValue);
}
//you should validate against a null clause....
//assign the item either to the relevant logical comparison expression list
if (queryFilterObject.LogicalOperator == "and" || queryFilterObject.LogicalOperator == "&&")
{
andExpressions.Add(clause);
}
else if (queryFilterObject.LogicalOperator == "or" || queryFilterObject.LogicalOperator == "||")
{
orExpressions.Add(clause);
}
}
if (andExpressions.Count > 0)
andExpressionBody = andExpressions.Aggregate((e, next) => Expression.And(e, next));
if (orExpressions.Count > 0)
orExpressionBody = orExpressions.Aggregate((e, next) => Expression.Or(e, next));
if (andExpressionBody != null && orExpressionBody != null)
bodyExpression = Expression.OrElse(andExpressionBody, orExpressionBody);
if (andExpressionBody != null && orExpressionBody == null)
bodyExpression = andExpressionBody;
if (andExpressionBody == null && orExpressionBody != null)
bodyExpression = orExpressionBody;
if (bodyExpression == null)
throw new Exception("Null Expression.");
var finalExpression = Expression.Lambda<Func<WorkOrder, bool>>(bodyExpression, parameterExpression);
return finalExpression;
}
public class QueryFilterObject
{
public string PropertyName { get; set; }
public Type PropertyType { get; set; }
public object Value { get; set; }
public string ComparisonOperator { get; set; }
public string LogicalOperator { get; set; }
}
In my case I return an expression of func (Expression<Func<TSource, bool>>
) instead of func itself (Func<TSource, bool>
). This allows my where clause to remain as an Iqueryable otherwise returning Func<TSource, bool>
instead of *Expression<Func<TSource, bool>>*
will turn your where clause into an ienumerable.
Finally, I just call my expression like below:
IQueryable<MyDTO> myDataSource = context.MyDataSource;
var filter = SimpleComparison(queryFilterObjects);
if (filter != null)
myDataSource = myDataSource.Where(filter);
//perfom other operations such as order by
return myDataSource.ToList();
Upvotes: 2
Reputation: 1800
You could change your function to instead of returning the compiled lambda have it return the base expression. Once you have those expressions stored in list, you could then use the Linq Aggregate()
function to build your final predicate.
public Func<TSource, bool> CreatePredicate<TSource>(IEnumerable<Expression> expressions)
{
var parameter = Expression.Parameter(typeof(TSource), "x");
var body = expressions.Aggregate((e, next) => Expression.AndAlso(e, next));
var predicate = Expression.Lambda<Func<TSource, bool>>(body, parameter).Compile();
return predicate;
}
This assumes you are wanting all of your statements to be joined using &&
. If you want them joined using ||
use Expression.OrElse
in place of Expression.AndAlso
. If there is a mix of and filters and or filters, the solution becomes considerably more complex.
Your example above would then become
var filters = request.Filter.Filters;
IQueryable<MyDTO> myDataSource = context.MyDataSource;
var expressions = new List<Expression>();
foreach (var filter in filters)
{
expressions.Add(SimpleComparison<MyDTO>(filter.Name, filter.Value));
}
var predicate = CreatePredicate<MyDTO>(expressions);
return myDataSource.Where(predicate);
Upvotes: 1
Reputation:
You're over-thinking things. You don't need to combine your expressions at all. The only complicated part is implementing SimpleComparison
, but you've done that already. Well, mostly. You should be returning Expression<Func<...>>
, not Func<...>
, so it should be
public Expression<Func<TSource, bool>> SimpleComparison<TSource>(string property, object value)
{
// ...
return Expression.Lambda<Func<TSource, bool>>
(Expression.Equal(propertyReference, constantReference),
new[] { pe });
}
Once you have that, you can chain the filters by calling Where
repeatedly, like so:
var filters = request.Filter.Filters;
IQueryable<MyDTO> query = context.MyDataSource;
foreach (var filter in filters)
query = query.Where(SimpleComparison<MyDTO>(filter.Name, filter.Value));
return query;
Upvotes: 4