Search code examples
c#linqlinq-to-entitiesentity-framework-6expression-trees

Adding Expression argument as property in LINQ to Entities


Using EF6, how would I bind a given Expression<Func<Row, string>> argument to an existing select expression, without having to rewrite every property binding using expression trees?

public IEnumerable<RowModel> GetRowModels(Expression<Func<Row, string>> textExpr)
{
    return from row in MyDatabaseContext.MyTable
           select new RowModel
           {
               RowID = row.ID,
               CreatedDate = row.CreatedDate,
               AnotherProperty = row.AnotherProperty,
               Text = textExpr, // how do I bind this expression?
               Value = row.OtherStuff.Where(os => os.ShouldUse).Select(os => os.Value).FirstOrDefault(),
               AnotherValue = row.OtherStuff.Where(os => os.ShouldUseAgain).Select(os => os.Value).FirstOrDefault()
           };
}

Solution

  • What you need here is a method to combine several expressions. Specifically, what we would like is a way to take an expression that maps a value and then also accept an expression that accepts the input of the first expression, and the output of the first expression, and computes a new value.

    As an implementation of this method we can replace all instances of "the result of the first function" with the body of the first function; after that all that needs to be done is to ensure that both expressions are using the same Parameter instance.

    public static Expression<Func<TFirstParam, TResult>>
        Combine<TFirstParam, TIntermediate, TResult>(
        this Expression<Func<TFirstParam, TIntermediate>> first,
        Expression<Func<TFirstParam, TIntermediate, TResult>> second)
    {
        var param = Expression.Parameter(typeof(TFirstParam), "param");
    
        var newFirst = first.Body.Replace(first.Parameters[0], param);
        var newSecond = second.Body.Replace(second.Parameters[0], param)
            .Replace(second.Parameters[1], newFirst);
    
        return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
    }
    

    The following code is used to replace all instances of an expression with another:

    public static Expression Replace(this Expression expression,
        Expression searchEx, Expression replaceEx)
    {
        return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
    }
    internal class ReplaceVisitor : ExpressionVisitor
    {
        private readonly Expression from, to;
        public ReplaceVisitor(Expression from, Expression to)
        {
            this.from = from;
            this.to = to;
        }
        public override Expression Visit(Expression node)
        {
            return node == from ? to : base.Visit(node);
        }
    }
    

    As for using the function; it's simple enough. We call Combine on your textExpression, and then we can create a lambda accepting both the row and the text result of the first expression as parameters. This lets you write a lambda that's almost exactly like the one you already have, but where you can use the text parameter to assign the Text value:

    public IEnumerable<RowModel> GetRowModels(
        Expression<Func<Row, string>> textExpr)
    {
        return MyDatabaseContext.MyTable.Select(
            textExpr.Combine((row, text) => new RowModel
        {
            RowID = row.ID,
            CreatedDate = row.CreatedDate,
            AnotherProperty = row.AnotherProperty,
            Text = text, // how do I bind this expression?
            Value = row.OtherStuff.Where(os => os.ShouldUse)
                .Select(os => os.Value).FirstOrDefault(),
            AnotherValue = row.OtherStuff.Where(os => os.ShouldUseAgain)
                .Select(os => os.Value).FirstOrDefault()
        }));
    }