Use a saved expression inside another expression

Viewed 62

Is it possible to do something like this:

        public static Expression<Func<EntityA, bool>> IsAGood =
            x => x.A != null &&
            x.A.valid;

        public static Expression<Func<EntityB, bool>> IsBGood=
            x => IsAGood (x.EntityA);

        var res = context.EntitiesB
                 .Where(x => x.count > 0)
                 .Where(IsBGood)

I know I can compile IsAGood and run it on x.EntityA but it loads the data to memory and I don't want to do this yet. Is there a way to do this without loading to memory?

2 Answers

Yes, you can do it with some expression trees processing. Maybe there are easier ways to do this, but the one I know looks something like this (you can place this code in static constructor):

Expression<Func<EntityB, EntityA>> exp = b => b.EntityA;
var param = exp.Parameters.First();
var expression = new ReplacingExpressionVisitor(new[]{IsAGood.Parameters.First()}, new []{exp.Body}).Visit(IsAGood.Body);
IsBGood = Expression.Lambda<Func<EntityB, bool>>(expression, param);

ReplacingExpressionVisitor is available since EF Core 3.0, if you are using an older version you can write you own one, it should not be that hard.

Also you can try using Expression.Inkove (but I think I had some issues with it being translated previously):

Expression<Func<EntityB, EntityA>> exp = b => b.EntityA;
var param = exp.Parameters.First();
IsBGood = Expression.Lambda<Func<EntityB, bool>>(Expression.Invoke(IsAGood, exp.Body), param);

Note that you cannot call Compile on a lambda expression in this context, because this creates a delegate. But EF needs an Expression because the o/r-mapper needs the syntax information it contains to convert it into a SQL command. An executable delegate cannot be convert to SQL and cannot be executed by the DB.


Now to your concern about loading data into memory. Neither IsAGood nor IsBGood nor your combined query loads any data into memory until you actually enumerate the query (e.g. with .ToList() or foreach).

You can even add parts dynamically as shown here:

var query = context.EntitiesB
    .Where(x => x.count > 0); // No data access happens here.
if (useCondition) {
    query = query.Where(IsBGood); // No data access happens here.
}
var result = query.ToList(); // The DB will be accessed here once.

Note that all the parts of your code only set up a query. They don't execute any query. Therefore I changed the name of the var from res to query.


Okay I see the problem. IsAGood cannot be called like this, since it is an Expression<>, not a delegate (and a delegate does not work anyway as I have explained earlier).

You can solve this by rewriting the expression as @GuruStron explains or change the test to

public static Expression<Func<EntityB, bool>> IsAinBGood =
        b => b.EntityA.A != null &&
             b.EntityA.A.valid;
Related