Reputation: 617
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
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