vladek
vladek

Reputation: 617

"The parameter was not bound in the specified LINQ to Entities query expression." Specification Pattern And

I follow the specification pattern implementation described here. I have a repository method looking like this:

public IEnumerable<MyDto> Find(Specification<Dto> specification)
{
    return myDbContext.MyDtos.Where(specification.ToExpression()).Take(20).ToList();
}

If I use a normal, non-composite specification it works just fine, but the following scenario fails with message "The parameter 'r' was not bound in the specified LINQ to Entities query expression.":

Specification<MyDto> spec = new Spec1(someCriterion)
    .And(new Spec2(someCriterion))
    .And(new Spec3(someCriterion))
    // etc...

var myDtos = repo.Find(spec);

From what I could read so far it has something to do with the parameter reference not being the same for all expressions, but I am not really sure how to fix this issue.

For reference, this is how the AndSpecification<T> class looks like in my code:

public class AndSpecification<T> : Specification<T>
{
    private readonly Specification<T> _left;
    private readonly Specification<T> _right;

    public AndSpecification(Specification<T> left, Specification<T> right)
    {
        _left = left;
        _right = right;
    }

    public override Expression<Func<T, bool>> ToExpression()
    {
        Expression<Func<T, bool>> leftExpression = _left.ToExpression();
        Expression<Func<T, bool>> rightExpression = _right.ToExpression();

        BinaryExpression andExpression = Expression.AndAlso(
            leftExpression.Body, rightExpression.Body);

        return Expression.Lambda<Func<T, bool>>(
            andExpression, leftExpression.Parameters.Single());
    }
}

Upvotes: 1

Views: 364

Answers (1)

canton7
canton7

Reputation: 42245

The problem is in your ToExpression method.

leftExpression and rightExpression are each a LambdaExpression, and each have their own, distinct T parameter.

When you create the LambdaExpression you return from ToExpression, you say that this should use the parameter from leftExpression. But what about the parameter that's used in rightExpression? rightExpression.Body contains expressions which use rightExpression.Parameters[0], and they'll still continue to reference the object rightExpression.Parameters[0] even after you take rightExpression.Body and put it in another expression.

You need to rewrite rightExpression to use the same parameter as leftExpression. The easiest way to do this is using an ExpressionVisitor.

First, create an ExpressionVisitor which simply replaces one parameter with another:

public class ParameterReplaceVisitor : ExpressionVisitor
{
    private readonly ParameterExpression target;
    private readonly ParameterExpression replacement;

    public ParameterReplaceVisitor(ParameterExpression target, ParameterExpression replacement) =>
        (this.target, this.replacement) = (target, replacement);

    protected override Expression VisitParameter(ParameterExpression node) =>
        node == target ? replacement : base.VisitParameter(node);
}

Then use this to rewrite your rightExpression.Body, so it uses the same parameter object as leftExpression:

var visitor = new ParameterReplaceVisitor(rightExpression.Parameters[0], leftExpression.Parameters[0]);
var rewrittenRightBody = visitor.Visit(rightExpression.Body.Visit);
var andExpression = Expression.AndAlso(leftExpression.Body, rewrittenRightBody);

return Expression.Lambda<Func<T, bool>>(
    andExpression, leftExpression.Parameters[0]);

Upvotes: 3

Related Questions