Building nested conditional expression-trees
Asked Answered
W

2

7

I'm trying to dynamically build some sql-queries depending on a given config to only query data needed:

When writing plain linq it would look like this:

var data = dbContext
.TableOne
.Select(t1 => new TableOneSelect
{
    TableOneId = t1.TableOneId,
    TableOneTableTwoReference = new[] { TableOne.FirstTableTwoReference.Invoke(t1) }
        .Select(t2 => new TableTwoSelect
        {
            TableTowId = (Guid?)t2.TableTwoId,
            // ... some more properties of t2
        }).FirstOrDefault(),
    // ... some more properties of t1
});

whereas TableOne.FirstTableTwoReference.Invoke(t1) is defined

public static Expression<Func<TableOne, TableTwo>> FirstTableTwoReference => (t1) => t1.TableTwoReferences.FirstOrDefault();

Currently I have the following for building the TableOne-part dynamically:

public Expression<Func<TableOne, TableOneSelect>> Init(TableOneConfig cfg)
{
    var memberBindings = new List<MemberBinding>();
    var selectType = typeof(TableOneSelect);
    var newExpression = Expression.New(selectType);
    var theEntity = Expression.Parameter(typeof(TableOne), "t1");

    // decide if the property is needed and add to the object-initializer
    if (cfg.Select("TableOneId"))
        memberBindings.Add(Expression.Bind(selectType.GetProperty("TableOneId"), Expression.Property(theEntity, nameof("TableOneId"))));

    // ... check other properties of TableOneSelect depending on given config

    var memberInit = Expression.MemberInit(newExpression, memberBindings);
    return Expression.Lambda<Func<tblTournament, EventResourceSelect>>(memberInit, theEntity);
}

same for TableTwo (different properties and different db-table).

This I can dynamically invoke like this

dbContext.TableOne.Select(t => TableOneHelper.Init(cfg).Invoke(t1));

whereas Invoke is the one from LinqKit.

But I get stuck with the inner part for the TableOneTableTwoReference where I need to make an enumeration to call the Init of TableTwoHelper but I don't get the point how this can be achieved.

I guess Expression.NewArrayInit(typeof(TableTwo), ...) would be step one. But I still get stuck in how to pass t1.TableTwoReferences.FirstOrDefault() to this array calling the Select on.

Windproof answered 31/3, 2017 at 11:10 Comment(2)
Something that may help you is to remember that x.FirstOrDefault() is equivalent to Enumerable.FirstOrDefault(x). Then you can use Expression.Call().Hurryscurry
@Hurryscurry My real problem it is even more complicated as of the first FirstOrDefault() is an Expression<Func<TableOne, TableTwo>> as well and I need to invoke this to fill the array. Will update my questionWindproof
R
2

I guess Expression.NewArrayInit(typeof(TableTwo), ...) would be step one. But I still get stuck in how to pass t1.TableTwoReferences.FirstOrDefault() to this array calling the Select on.

As I understand, the question is what is the expression equivalent of

new[] { TableOne.FirstTableTwoReference.Invoke(t1) }

It's really simple. As you correctly stated, you'll need Expression.NewArrayInit expression. However, since it expects params Expression[] initializers, instead of LINQKit Invoke extension method you should use Expression.Invoke method to emit call to TableOne.FirstTableTwoReference lambda expression with the outer theEntity ("t1") parameter:

var t2Array = Expression.NewArrayInit(
    typeof(TableTwo),
    Expression.Invoke(TableOne.FirstTableTwoReference, theEntity));

The same way you can emit the Select expression:

var t2Selector = TableTwoHelper.Init(cfg2);
// t2Selector is Expression<Func<TableTwo, TableTwoSelect>>
var t2Select = Expression.Call(
    typeof(Enumerable), "Select", new[] { t2Selector.Parameters[0].Type, t2Selector.Body.Type },
    t2Array, t2Selector);

then FirstOrDefault call:

var t2FirstOrDefault = Expression.Call(
    typeof(Enumerable), "FirstOrDefault", new[] { t2Selector.Body.Type },
    t2Select);

and finally the outer member binding:

memberBindings.Add(Expression.Bind(
    selectType.GetProperty("TableOneTableTwoReference"),
    t2FirstOrDefault));

This will produce the equivalent of your "plain linq" approach.

Recti answered 12/4, 2017 at 20:7 Comment(2)
Out of interest: You are writing Expression.Call(typeof(Enumerable), "Select",... but this is not working at all. Always getting No generic method 'Select' on type 'System.Linq.Enumerable' is compatible with the supplied type arguments and arguments. What's the best workaround for?Windproof
@Windproof Not sure why are you getting that error. I've tested it before posting and it worked. t2Selector.Parameters[0].Type is TableTwo, t2Selector.Body.Type is TableTwoSelect, so we have Enumerable.Select<TableTwo, TableTwoSelect>(TableTwo[] array, Func<TableTwo, TableTwoSelect> t2Selector) which is correct call.Recti
P
0

Add the member binding...

memberBindings.Add(Expression.Bind(selectType.GetProperty("TableOneTableTwoReference"), BuildTableTwoExpression(theEntity)));

...and then build TableTwo's expression

private Expression BuildTableTwoExpression(ParameterExpression t1)
{
    var arrayEx = Expression.NewArrayInit(typeof(TableTwo), Expression.Invoke(TableOne.FirstTableTwoReference, t1));

    Expression<Func<TableTwo, TableTwoSelect>> selector = (t2 => new TableTwoSelect
    {
        TableTowId = (Guid?)t2.TableTwoId,
        // ... some more properties of t2
    });

    Expression<Func<IEnumerable<TableTwo>, TableTwoSelect>> selectEx =
        ((t1s) => Enumerable.Select(t1s, selector.Compile()).FirstOrDefault());

    return Expression.Invoke(selectEx, arrayEx);
}
Protect answered 12/4, 2017 at 20:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.