2

I have two expressions and I want to chain them so the resulting expression contains both input expressions.

Expression<Func<IQueryable<Material>, object>> expression1 = x => x.Include(m => m.MaterialGroup);
Expression<Func<IQueryable<Material>, object>> expression2 = x => x.Include(m => m.MaterialSomething);

var expression3 = expression1.Update(expression2.Body, expression2.Parameters);

Right now expression3 only contains x => x.Include(m => m.MaterialSomething) so it overrides the second expression. I'd like it to be x => x.Include(m => m.MaterialGroup).Include(m => m.MaterialSomething).

What I intend to achieve is to programatically join multiple include expressions in order to be able to build more efficient system for eager loading in EF Core.

EDIT: This is not a matter of ANDing, ORing etc. because I want these expressions to be chained (like a dot chaining), not logically joined.

Daniel

Aranha
  • 2,903
  • 3
  • 14
  • 29
  • Possible duplicate of [Combining two expressions (Expression>)](https://stackoverflow.com/questions/457316/combining-two-expressions-expressionfunct-bool) – Mehdi Dehghani Feb 25 '19 at 08:21
  • Nope, I read that one a few times - it doesn't chain expressions, it ANDs them and so on. – Aranha Feb 25 '19 at 08:26
  • @Aranha those aren't queries or lambas, just eager load statements. There's no joining or ANDing here. Just call one after the other as you posted with `mySet.Include(m => m.MaterialGroup).Include(m => m.MaterialSomething)`. Or use the *exact equivalent* `var q1=mySet.Include(m => m.MaterialGroup); q1=q1.Include(m => m.MaterialSomething)` – Panagiotis Kanavos Feb 25 '19 at 08:38
  • 1
    @Aranha what is the *actual* problem you want to solve? It seems you got stuck because you tried using expressions over *functions* instead of IQueryable and queries, turning what should be a simple chained call into something a lot more complex – Panagiotis Kanavos Feb 25 '19 at 08:41
  • I want to be able to create multiple ``IncludeDefinition``s which all can be represented by something like this: (pseudo code): ``var materialHeaderIncludes = new IncludeDefinition(x => x.Include(m => m.MaterialGroup));`` - imagine I have four definitions for every model. These definitions are hierarchical, so I have a Summary that derives from Header, Description from Summary and Detail from Description. I want these definitions to add new includes but also to inherit includes from their superclasses. – Aranha Feb 25 '19 at 21:33
  • Also, I don't have an object that contains a list of models because these definitions are meant to be static and reusable during application lifetime so I can't just chain them like you're trying to show. I know what my problem is so please don't undermine my question. – Aranha Feb 25 '19 at 21:35

2 Answers2

0

Because Include is extension method your expression

x => x.Include(m => m.MaterialGroup);

actually is

x => QueryableExtensions.Include(x, m => m.MaterialGroup);

So to chain your expressions you need to replace first argument of Include with call to another Include

x => QueryableExtensions.Include(
  QueryableExtensions.Include(x, m => m.MaterialSomething),
  m => m.MaterialGroup);

Next code will do this chaining

public static Expression<Func<IQueryable<T>, object>> Chain<T>(
  params Expression<Func<IQueryable<T>, object>>[] expressions)
{
    if (expressions.Length == 0)
        throw new ArgumentException("Nothing to chain");

    if (expressions.Length == 1)
        return expressions[0];

    Expression body = expressions[0].Body;
    var parameter = expressions[0].Parameters[0];
    foreach (var expression in expressions.Skip(1))
    {
        var methodCall = (MethodCallExpression)expression.Body;
        var lambda = (UnaryExpression)methodCall.Arguments[1];

        body = Expression.Call(typeof(QueryableExtensions),
            "Include",
            new []{ typeof(T), ((LambdaExpression)lambda.Operand).Body.Type},
            body, lambda
            );
    }

    return Expression.Lambda<Func<IQueryable<T>, object>>(body, parameter);
}

Usage:

var expression = Chain(expression1, expression2 /*, expression3 .... */);

You can test it online here

Please note that this code skip expression validation for brevity.

Aleks Andreev
  • 7,016
  • 8
  • 29
  • 37
  • I changed typeof(QueryableExtensions) to typeof(EntityFrameworkQueryableExtensions) and it works fine :) Thank you ! – Aranha Feb 26 '19 at 08:37
  • @Aranha I guess you can fix it if you check `methodCall.Method.Name` and put this value into `Expression.Call` – Aleks Andreev Feb 26 '19 at 11:19
  • Tried that, throws exception ``'No generic method 'ThenInclude' on type 'Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions' is compatible with the supplied type arguments and arguments`` - the signature is different and different parameters need to be supplied I guess. – Aranha Feb 26 '19 at 11:51
  • @Aranha It's look like we are using diferent versions of EF. My desktop version does not have method `ThenInclude` so I can't produce verified answer right now. From EF Core examples I see that you're right: `ThenInclude` works with different type of parameter. I'll try to adapt my answer for this later – Aleks Andreev Feb 26 '19 at 13:04
  • Ok, I'm working on this as well in the meantime. Consider this: ``{x => x.Include(s => s.StockForOrderLines)}`` and ``{x => x.Include(s => s.StockDetails).ThenInclude(sd => sd.Material)}``. What I need is: ``{x => x.Include(s => s.StockForOrderLines).Include(s => s.StockDetails).ThenInclude(sd => sd.Material)}``. Of course if I inverse the parameters, it should work exactly the same and in both cases it should be possible to add new ``Include`` with optional ``ThenInclude``. – Aranha Feb 26 '19 at 13:20
0

I'd like to add another way to archive chaining lambda expressions:

add the follow static method somewhere easy access

public static Expression<Func<T, bool>> ConcatLambdaExpression<T>(Expression<Func<T, bool>> firstExpression, Expression<Func<T, bool>> secondExpression)
{
    var invokedThird = Expression.Invoke(secondExpression, firstExpression.Parameters.Cast<Expression>());
    var finalExpression = Expression.Lambda<Func<T, bool>>(Expression.AndAlso(firstExpression.Body, invokedThird), firstExpression.Parameters);
    return finalExpression;
}

Then you can use it on this way:

public PersonDTO GetAll()
{
    Expression<Func<Person, bool>> expression = x => x != null;
    expression = x => x.Name == "John";

    Expression<Func<Person, bool>> pred = x => x.LastName == "Doe" || x.LastName == "Wick";

    //result of expression would be:  
    ////expression = x.Name == "John" && (x => x.LastName == "Doe" || x.LastName == "Wick")

    expression = Utilities.ConcatLambdaExpression(expression, pred);

    var result = Context.PersonEntity.Where(expression);

    //your code mapping results to PersonDTO
    ///resultMap...            

    return resultMap;
}
barbsan
  • 3,418
  • 11
  • 21
  • 28
RogerEdward
  • 121
  • 1
  • 7