user1477388
user1477388

Reputation: 21420

Building a list of expressions via Expression Trees

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

Answers (3)

MG89
MG89

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

Bradford Dillon
Bradford Dillon

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

user743382
user743382

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

Related Questions