У меня изначально были подозрения, но теперь я могу подтвердить это.
Вы объединяете две лямбды, которые имеют два совершенно разных экземпляра своих параметров. Экземпляры параметров не могут быть заменены, даже если они имеют одинаковые имена и типы. Они являются эффективными параметрами в разных областях. Когда вы попытались вызвать одно из выражений с неверным параметром объекта, в этом случае возникает хаос, в этом случае переполнение стека.
Что вам следует сделать, это создать новый экземпляр параметра (или повторно использовать его) и заново привязать тела ваших лямбд, чтобы использовать этот новый параметр. Я подозреваю, что это исправит. И чтобы пойти дальше, вы должны правильно объединить эти выражения, перестроив их, а не соединяя их вместе как вызовы методов. Я сомневаюсь, что поставщикам запросов это понравится как вызовам.
Попробуйте выполнить эту реализацию ваших And()
и Or()
методов вместе с этим вспомогательным методом, чтобы выполнить повторное связывание:
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
{
// reuse the first expression's parameter
var param = expression1.Parameters.Single();
var left = expression1.Body;
var right = RebindParameter(expression2.Body, expression2.Parameters.Single(), param);
var body = Expression.AndAlso(left, right);
return Expression.Lambda<Func<T, bool>>(body, param);
}
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
{
var param = expression1.Parameters.Single();
var left = expression1.Body;
var right = RebindParameter(expression2.Body, expression2.Parameters.Single(), param);
var body = Expression.OrElse(left, right);
return Expression.Lambda<Func<T, bool>>(body, param);
}
private static Expression RebindParameter(Expression expr, ParameterExpression oldParam, ParameterExpression newParam)
{
switch (expr.NodeType)
{
case ExpressionType.Parameter:
var asParameterExpression = expr as ParameterExpression;
return (asParameterExpression.Name == oldParam.Name)
? newParam
: asParameterExpression;
case ExpressionType.MemberAccess:
var asMemberExpression = expr as MemberExpression;
return asMemberExpression.Update(
RebindParameter(asMemberExpression.Expression, oldParam, newParam));
case ExpressionType.AndAlso:
case ExpressionType.OrElse:
case ExpressionType.Equal:
case ExpressionType.NotEqual:
case ExpressionType.LessThan:
case ExpressionType.LessThanOrEqual:
case ExpressionType.GreaterThan:
case ExpressionType.GreaterThanOrEqual:
var asBinaryExpression = expr as BinaryExpression;
return asBinaryExpression.Update(
RebindParameter(asBinaryExpression.Left, oldParam, newParam),
asBinaryExpression.Conversion,
RebindParameter(asBinaryExpression.Right, oldParam, newParam));
case ExpressionType.Call:
var asMethodCallExpression = expr as MethodCallExpression;
return asMethodCallExpression.Update(
RebindParameter(asMethodCallExpression.Object, oldParam, newParam),
asMethodCallExpression.Arguments.Select(arg =>
RebindParameter(arg, oldParam, newParam)));
case ExpressionType.Invoke:
var asInvocationExpression = expr as InvocationExpression;
return asInvocationExpression.Update(
RebindParameter(asInvocationExpression.Expression, oldParam, newParam),
asInvocationExpression.Arguments.Select(arg =>
RebindParameter(arg, oldParam, newParam)));
case ExpressionType.Lambda:
var asLambdaExpression = expr as LambdaExpression;
return Expression.Lambda(
RebindParameter(asLambdaExpression.Body, oldParam, newParam),
asLambdaExpression.Parameters.Select(param =>
(ParameterExpression)RebindParameter(param, oldParam, newParam)));
default:
// you should add cases for any expression types that have subexpressions
return expr;
}
}
Метод перепривязки выполняет поиск (по имени) и возвращает выражение, в котором все ParameterExpression
в дереве выражений заменяются экземпляром другого ParameterExpression
. Это не изменяет существующие выражения, но перестраивает выражение, создавая при необходимости новые обновленные выражения. Другими словами, он возвращает новое выражение, которое следует использовать вместо того, которое вы связываете.
Идея состоит в том, чтобы изучить Expression
и определить, какой это тип. Если это ParameterExpression
, проверьте, имеет ли оно то же имя, что и параметр, который мы ищем. Если это так, верните наш новый параметр, иначе верните его, поскольку мы не должны его менять. Если выражение не является параметром, оно, вероятно, будет выражением, которое содержит подвыражения и должно быть заменено.
A BinaryExpression
будет иметь операнд Left
и операнд Right
, оба выражения. Они оба должны быть восстановлены, так как где-то внизу их деревьев выражений может быть параметр, который нужно заменить. Метод Update()
заменит текущее выражение аналогичным новым подвыражением. В этом случае мы только хотели (потенциально) обновить подвыражения Left
и Right
.
MethodCallExpression
и InvocationExpression
имеют ту же идею, но их дерево немного отличается. У него есть выражение Object
(или Expression
в случае вызова), которое представляет экземпляр (или делегат / лямбда), который вы хотите вызывать. (MethodCallExpression
также имеет MethodInfo
, который представляет метод экземпляра для вызова). У них также есть Arguments
(все выражения), которые используются в качестве аргументов для вызова. Эти выражения потенциально должны быть восстановлены.
Вы можете думать о методе RebindParameter()
как о «супер» - Update()
методе, который обновляет параметры в пределах всего дерева выражений.
Чтобы дополнительно проиллюстрировать, иллюстрацию, которая поможет визуализировать, как выглядит дерево и что меняется. Обратите внимание, что поскольку здесь происходят замены, большинство поддеревьев будут новыми экземплярами.
[
Теперь вот что-то, чего я не знал, было доступно, ExpressionVisitor
. Жаль, что я заметил это раньше. Это сделает ребиндер лучше работать с. Вместо того, чтобы публиковать полный код здесь, здесь он находится на pastebin . Затем использовать его:
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
{
// reuse the first expression's parameter
var param = expression1.Parameters.Single();
var left = expression1.Body;
var right = ParameterRebinder.Rebind(expression2.Body, expression2.Parameters.Single(), param);
var body = Expression.AndAlso(left, right);
return Expression.Lambda<Func<T, bool>>(body, param);
}
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
{
var param = expression1.Parameters.Single();
var left = expression1.Body;
var right = ParameterRebinder.Rebind(expression2.Body, expression2.Parameters.Single(), param);
var body = Expression.OrElse(left, right);
return Expression.Lambda<Func<T, bool>>(body, param);
}