Combining multiple expressions trees

.net c# entity-framework expression-trees

Question

I'm getting the following error

The parameter 'p' was not bound in the specified LINQ to Entities query expression.

I understand the problem (same instance of ParameterExpression should be used with all the expressions in the tree) and have attempted to use solutions I've found online but with no luck.

This is my method

private void SeedEntity<TEntity>(DatabaseContext context, ref TEntity entity, params Expression<Func<TEntity, object>>[] identifierExpressions) where TEntity : class
{
    Expression<Func<TEntity, bool>> allExpresions = null;

    var parameters = identifierExpressions.SelectMany(x => x.Parameters).GroupBy(x => x.Name).Select(p => p.First()).ToList();

    foreach (Expression<Func<TEntity, object>> identifierExpression in identifierExpressions)
    {
        Func<TEntity, object> vv = identifierExpression.Compile();
        object constant = vv(entity);

        ConstantExpression constExp = Expression.Constant(constant, typeof(object));
        BinaryExpression equalExpression1 = Expression.Equal(identifierExpression.Body, constExp);
        Expression<Func<TEntity, bool>> equalExpression2 = Expression.Lambda<Func<TEntity, bool>>(equalExpression1, parameters);

        if (allExpresions == null)
        {
            allExpresions = equalExpression2;
        }
        else
        {
            BinaryExpression bin = Expression.And(allExpresions.Body, equalExpression2.Body);
            allExpresions = Expression.Lambda<Func<TEntity, bool>>(bin, parameters);
        }
    }

    TEntity existingEntity = null;
    if (allExpresions != null)
    {
        existingEntity = context.Set<TEntity>().FirstOrDefault(allExpresions);
    }

    if (existingEntity == null)
    {
        context.Set<TEntity>().Add(entity);
    }
    else
    {
        entity = existingEntity;
    }
}

It generates an expression for the lookup of an entity based on a number of properties.

It works fine for a single expression, the error only occurs when passing in multiple.

Called like this:

SeedEntity(context, ref e, p=> p.Name);//Works
SeedEntity(context, ref e, p=> p.Name, p=> p.Age);//Fails

It generates something similar to me performing the following:

context.Set<TEntity>().FirstOrDefault(p=>p.Name == e.Name && p.Age == e.Age);

Replacing e.Name && e.Age with a ConstantExpression

You can see in the method above I grab all of the unique params and store them in parameters at the top, then use the same variable throughout.This is the start, but then I need to replace the instances of the parameter in each of the Expression<Func<TEntity, bool>> passed in as the params array, this is where I'm failing.

I've tried enumerate the expressions and use the .Update() method passing in the params

I also tried a solution using the ExpressionVisitor

public class ExpressionSubstitute : ExpressionVisitor
{
    public readonly Expression from, to;
    public ExpressionSubstitute(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        if (node == from) return to;
        return base.Visit(node);
    }
}

public static class ExpressionSubstituteExtentions
{
    public static Expression<Func<TEntity, TReturnType>> RewireLambdaExpression<TEntity, TReturnType>(Expression<Func<TEntity, TReturnType>> expression, ParameterExpression newLambdaParameter)
    {
        var newExp = new ExpressionSubstitute(expression.Parameters.Single(), newLambdaParameter).Visit(expression);
        return (Expression<Func<TEntity, TReturnType>>)newExp;
    }
}

Accepted Answer

You're really close. I don't see the point of your parameters variable. Grouping them by name is a mistake. Why not just pass the parameters from the expression? Then visit if necessary. Your visitor code is fine.

    private static void SeedEntity<TEntity>(DbContext context, ref TEntity entity, params Expression<Func<TEntity, object>>[] identifierExpressions) 
        where TEntity : class
    {
        Expression<Func<TEntity, bool>> allExpresions = null;

        foreach (Expression<Func<TEntity, object>> identifierExpression in identifierExpressions)
        {
            Func<TEntity, object> vv = identifierExpression.Compile();
            object constant = vv(entity);

            ConstantExpression constExp = Expression.Constant(constant, typeof(object));
            BinaryExpression equalExpression1 = Expression.Equal(identifierExpression.Body, constExp);
            Expression<Func<TEntity, bool>> equalExpression2 = Expression.Lambda<Func<TEntity, bool>>(equalExpression1, identifierExpression.Parameters);

            if (allExpresions == null)
            {
                allExpresions = equalExpression2;
            }
            else
            {
                var visitor = new ExpressionSubstitute(allExpresions.Parameters[0], identifierExpression.Parameters[0]);
                var modifiedAll = (Expression<Func<TEntity,bool>>)visitor.Visit(allExpresions);
                BinaryExpression bin = Expression.And(modifiedAll.Body, equalExpression2.Body);
                allExpresions = Expression.Lambda<Func<TEntity, bool>>(bin, identifierExpression.Parameters);
            }
        }

        TEntity existingEntity = null;
        if (allExpresions != null)
        {
            existingEntity = context.Set<TEntity>().FirstOrDefault(allExpresions);
        }

        if (existingEntity == null)
        {
            context.Set<TEntity>().Add(entity);
        }
        else
        {
            entity = existingEntity;
        }
    }


Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Is this KB legal? Yes, learn why