1

I'm new to expression building, so bear with me a little bit.

I'm building off of this answer, using PredicateBuilder for building a predicate with an unknown number of filters. Say I have these classes:

public FilterTerm
{
    public string ComparisonOperatorA { get; set; }
    public decimal ValueA { get; set; }
    public string ComparisonOperatorB { get; set; }
    public decimal ValueB { get; set; }
    public string ComparisonOperatorC { get; set; }
    public decimal ValueC { get; set; }
}

public Item
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal SkillLevelA { get; set; }
    public decimal SkillLevelB { get; set; }
    public decimal SkillLevelC { get; set; }
}

where FilterTerm.ComparisonOperator[A|B|C] can be any of >, >=, <, <=, ==, which a user submits N FilterTerms to filter results from the db (via EF context) Items. My main query is set up this way:

[...]

List<FilterTerm> filterTerms = GetFilterTerms();

var itemsQuery = context.GetAll<Item>(); // reutrns IQueryable<Item>
itemsQuery = itemsQuery.ApplyFilters(filterTerms);

var items = await itemsQuery.ToListAsync();

[...]

I'm building the base predicate like this, with missing knowledge (I'm using assigned variables in the loop so values aren't evaluated at execution time):

private IQueryable<Item> ApplyFilters(IQueryable<Item> query, FilterTerms filterTerms)
{ 
    Expression<Func<Item, bool>> superPredicate = PredicateBuilder.False<Item>()
    foreach (var filterTerm in filterTerms)
    {
        Expression<Func<Item, bool>> subPredicate = PredicateBuilder.True<Item>();

        subPredicate = subPredicate.And<Item>(GetDynamicPredicate(i => i.SkillLevelA, filterTerm.ValueA, filterTerm.ComparisonOperatorA));
        subPredicate = subPredicate.And<Item>(GetDynamicPredicate(i => i.SkillLevelB, filterTerm.ValueB, filterTerm.ComparisonOperatorB));
        subPredicate = subPredicate.And<Item>(GetDynamicPredicate(i => i.SkillLevelC, filterTerm.ValueC, filterTerm.ComparisonOperatorC));

        superPredicate = superPredicate.Or(subPredicate);
    }
    
    return query.Where(superPredicate);
}

private Expression<Func<Item, bool>> GetDynamicPredicate(XXXX xxxx, decimal value, string comparisonOperator)
{
    Expression<Func<Item, bool>> predicate;

    switch (comparisonOperator)
    {
        case "<":
            predicate = Expression.LessThan(xxxx, value);
            break;
        [...]
    }

    return predicate;
}

where I don't know what to have for XXXX xxxx, which I prefer to have as a term selector like i => i.[Property] (is it possible to do this in a way that maintains Query building before sending it to the DB for execution?), and I know that the Expression.LessThan requires Expression left, Expression right for the signature, but I'm stuck with how to complete this.

I've seen questions like this one, but I just I can't seem to figure out how to apply the answers to my use case. Can anyone help provide the missing knowledge? It's been a few hours of trial-and-error, and I may just need something spoon-fed to me to connect the dots... I think I'm missing some steps with BinaryExpression and Lambda building, but I just can't piece it together.

Gert Arnold
  • 105,341
  • 31
  • 202
  • 291
Daevin
  • 778
  • 3
  • 14
  • 31
  • 1
    IMHO create a dictionary of operator strings to `ExpressionType` values so you can call `Expression.MakeBinary` instead of a switch for each individual comparison type. (Note that `.MakeBinary` already has that switch statement https://github.com/dotnet/runtime/blob/537a547f74287a625055b0a57dc29ff548edb176/src/libraries/System.Linq.Expressions/src/System/Linq/Expressions/BinaryExpression.cs#L898) – Jeremy Lakeman Apr 13 '22 at 01:18
  • @JeremyLakeman I was going to be creating an Enum for operators to strongly type and ensure input, but this is waaaay better! I've been tasked with converting a bunch of SQL Server SPs into EF+LINQtoSQL, so as I've been going I've been building a library of helpful/reusable methods, and my god is that good to know. Cheers, thanks mate! – Daevin Apr 13 '22 at 13:27

1 Answers1

1

It can sometimes get a bit daunting when working with expression trees, but your use-case is not too hard. The key bit you need is how to fill in the arguments to LessThan. The short version is that it should look like this:

predicate = Expression.Lambda<Func<Item, bool>>(
    Expression.LessThan(property.Body, Expression.Constant(value)), property.Parameters[0]);

That requires you to change your parameter to match the incoming lambda which specifies a property, so the complete solution is for your method to look like:

private Expression<Func<Item, bool>> GetDynamicPredicate<TValue>(
    Expression<Func<Item, TValue>> property, decimal value, string comparisonOperator)
{
    Expression<Func<Item, bool>> predicate = null;

    switch (comparisonOperator)
    {
        case "<":
            predicate = Expression.Lambda<Func<Item, bool>>(
                Expression.LessThan(property.Body, Expression.Constant(value)),
                property.Parameters[0]);
            break;
    }

    return predicate;
}

Now for a breakdown of the changes we made.

First we changed the GetDynamicPredicate method so that the type of the first parameter is Expression<Func<Item, TValue>>, since a lambda requires an input and output type. That required adding the generic parameter TValue to your method.

Next we know that the incoming lambda is of the form x => x.Property. What we actually want is x => x.Property < value. Fortunately that's a really simple transformation. We know that the body is x.Property so we simply have to use that when calling LessThan, as that is what we're comparing against.

The value is easy: Expression.Constant will work just fine for most values.

Finally, we need the predicate variable to contain a lambda expression, so we need to call Expression.Lambda, grabbing the existing parameter from the incoming lambda for convenience.

Kirk Woll
  • 76,112
  • 22
  • 180
  • 195
  • 1
    Since property is `TValue`, `decimal value` should probably be the same. Otherwise you'd need to insert a conversion for the comparison to work. – Jeremy Lakeman Apr 13 '22 at 01:10
  • 1
    This is awesome, thank you so much! The breakdown explanation is amazing, it definitely helps me piece together what I was missing. Cheers, and thanks for the help! – Daevin Apr 13 '22 at 13:34
  • 1
    @JeremyLakeman, I agree, and there are several other ways I would generalize this code myself, but in terms of answering the OP's question, I was trying to minimize changes to the code so that the answer would be as clear as possible. That said, it's good advice. – Kirk Woll Apr 13 '22 at 16:35
  • 1
    @KirkWoll Indeed it is good advice, and I do appreciate your adapting to my circumstance for clarity. My current use-case is a specific `decimal` implementation, but my ultimate intent is to have this generalized, and so using `Expression> property, TValue value` in the signature will be what I end up doing. :) – Daevin Apr 13 '22 at 17:11
  • Hey @KirkWoll sorry to bug you with this again, but it turns out I need to apply this on a collection of `Item`s on an object `Course`, but when I try to do `query = query.Include(c => c.Items.Where(superPredicate.Compile()))` and I'm getting an error I don't understand the cause of. Can you check it out here: https://stackoverflow.com/questions/71876163/how-can-i-use-a-built-expression-in-an-include-where-to-apply-an-include-filt? – Daevin Apr 14 '22 at 18:30
  • 1
    @Daevin sure, I'll take a look at it. – Kirk Woll Apr 14 '22 at 18:49