Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Building nested conditional expression-trees

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.

like image 766
KingKerosin Avatar asked Mar 31 '17 11:03

KingKerosin


1 Answers

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.

like image 110
Ivan Stoev Avatar answered Sep 24 '22 23:09

Ivan Stoev