【问题标题】:Linq to SQL throwing a StackOverflowExceptionLinq to SQL 抛出 StackOverflowException
【发布时间】:2011-04-21 13:27:08
【问题描述】:

我正在使用 Linq to SQL 执行一个非常简单的查询。我正在创建表达式,然后将其传递给 Where() 扩展方法。当我尝试实际执行查询时,Linq 内部会抛出 StackOverflowException。代码如下:

int expectedCount = 4;
Expression<Func<Thing, bool>> expression = ...;

//Expression looks like (LocaleID = 1 && GenderID ==1 && (TimeFrameID == 2007 || TimeFrameID == 2008))

using (XYZDataContext context = new XYZDataContext())
{
    int count = context.Things.Where(expression).Count();
    //...
}

这里是表达式的DebugView:

.Lambda #Lambda1<System.Func`2[XYZ.DataAccess.Thing,System.Boolean]>(XYZ.DataAccess.Thing $o)
{
    .Invoke (.Lambda #Lambda2<System.Func`2[XYZ.DataAccess.Thing,System.Boolean]>)($o) & .Invoke (.Lambda #Lambda3<System.Func`2[XYZ.DataAccess.Thing,System.Boolean]>)($o)
}

.Lambda #Lambda2<System.Func`2[XYZ.DataAccess.Thing,System.Boolean]>(XYZ.DataAccess.Thing $o)
{
    .Invoke (.Lambda #Lambda4<System.Func`2[XYZ.DataAccess.Thing,System.Boolean]>)($o) & .Invoke (.Lambda #Lambda5<System.Func`2[XYZ.DataAccess.Thing,System.Boolean]>)($o)
}

.Lambda #Lambda3<System.Func`2[XYZ.DataAccess.Thing,System.Boolean]>(XYZ.DataAccess.Thing $o)
{
    .Invoke (.Lambda #Lambda6<System.Func`2[XYZ.DataAccess.Thing,System.Boolean]>)($o) | .Invoke (.Lambda #Lambda7<System.Func`2[XYZ.DataAccess.Thing,System.Boolean]>)($o)
}

.Lambda #Lambda4<System.Func`2[XYZ.DataAccess.Thing,System.Boolean]>(XYZ.DataAccess.Thing $o)
{
    $o.LocaleID == .Constant<System.Nullable`1[System.Int32]>(1)
}

.Lambda #Lambda5<System.Func`2[XYZ.DataAccess.Thing,System.Boolean]>(XYZ.DataAccess.Thing $o)
{
    $o.GenderID == .Constant<System.Nullable`1[System.Int32]>(1)
}

.Lambda #Lambda6<System.Func`2[XYZ.DataAccess.Thing,System.Boolean]>(XYZ.DataAccess.Thing $o)
{
    $o.TimeframeID == .Constant<System.Nullable`1[System.Int32]>(2007)
}

.Lambda #Lambda7<System.Func`2[XYZ.DataAccess.Thing,System.Boolean]>(XYZ.DataAccess.Thing $o)
{
    $o.TimeframeID == .Constant<System.Nullable`1[System.Int32]>(2008)
}

这个表达式对我来说似乎是正确的,而且相当简单。当我阅读调试视图时,我看到:

((LocaleID == 1 && GenderID == 1) && (TimeFrameID == 2007 || TimeFrameID == 2008))

...这是正确的。

更新 1

删除 一个 的内部 or'd 子句,它工作正常。因此,同时拥有内部 or'd 子句会以某种方式破坏从 LINQ 到 SQL 的转换。

更新 2

我无法让调试器单步执行 .NET Framework 代码 - 我已尝试使用 Reflector 以及仅使用 Visual Studio 来执行此操作。我进去过一次,但一般来说是行不通的。我确实遇到 StackOverflowException 的一次发生在:

ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, object state, bool ignoreSyncCtx)

更新 3

这是用于创建表达式的代码。有太多代码要发布,但它的核心在下面。我有允许我构建复杂的多级查询并将其序列化为 JSON 和 XML 的类。在核心,查询的每个部分都是使用以下方法构建的,然后是 Or'd 和 And'd 一起:

public class LinqSearchField<T, V> : ISearchField
{
    public string Name { get; private set; }
    public Expression<Func<T, V>> Selector { get; private set; }

    public Expression<Func<T, bool>> LessThan(V value)
    {
        return Expression.Lambda<Func<T, bool>>(Expression.LessThan(this.Selector.Body, GetConstant(value)), this.Selector.Parameters);
    }

    public Expression<Func<T, bool>> LessThanOrEqual(V value)
    {
        return Expression.Lambda<Func<T, bool>>(Expression.LessThanOrEqual(this.Selector.Body, GetConstant(value)), this.Selector.Parameters);
    }

    public Expression<Func<T, bool>> Equal(V value)
    {
        return Expression.Lambda<Func<T, bool>>(Expression.Equal(this.Selector.Body, GetConstant(value)), this.Selector.Parameters);
    }

    public Expression<Func<T, bool>> NotEqual(V value)
    {
        return Expression.Lambda<Func<T, bool>>(Expression.NotEqual(this.Selector.Body, GetConstant(value)), this.Selector.Parameters);
    }

    public Expression<Func<T, bool>> GreaterThan(V value)
    {
        return Expression.Lambda<Func<T, bool>>(Expression.GreaterThan(this.Selector.Body, GetConstant(value)), this.Selector.Parameters);
    }

    public Expression<Func<T, bool>> GreaterThanOrEqual(V value)
    {
        return Expression.Lambda<Func<T, bool>>(Expression.GreaterThanOrEqual(this.Selector.Body, GetConstant(value)), this.Selector.Parameters);
    }

    private ConstantExpression GetConstant(V value)
    {
        return Expression.Constant(value, typeof(V));
    }

    public Expression<Func<T, bool>> Null()
    {
        return Expression.Lambda<Func<T, bool>>(Expression.Equal(this.Selector.Body, Expression.Constant(null)), this.Selector.Parameters);
    }

    public Expression<Func<T, bool>> NotNull()
    {
        return Expression.Lambda<Func<T, bool>>(Expression.NotEqual(this.Selector.Body, Expression.Constant(null)), this.Selector.Parameters);
    }
}

这是 And 代码(OR 代码相同,但使用 Expression.And 代替):

public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expression1, Expression<Func<T, bool>> expression2)
{
    ParameterExpression[] parameters = expression1.Parameters.Union(expression2.Parameters).Distinct(new ParameterExpressionComparer()).ToArray();
    InvocationExpression invocationExpression1 = Expression.Invoke(expression1, parameters);
    InvocationExpression invocationExpression2 = Expression.Invoke(expression2, parameters);
    Expression binaryExpression = null;

    //And the current expression to the previous one.
    binaryExpression = Expression.AndAlso(invocationExpression1, invocationExpression2); //Or OrElse.

    //Wrap the expression in a lambda.
    return Expression.Lambda<Func<T, bool>>(binaryExpression, parameters);
}

更新 4

它可能会不受欢迎,但这里是sample which reproduces this issue。我真的需要弄清楚这里发生了什么。

【问题讨论】:

  • 如果您在AreEqual 方法之外评估expression 是否也会出现Exception
  • 是的,我会从这里的代码中删除该部分以使其更清晰。
  • 您是否介意向我们展示您是如何实际构建表达式的。我怀疑您的构建不正确,因此会导致问题。您向我们展示的内容似乎与调试视图不对应,并且似乎由 lambdas 组成。
  • 我在上面添加了更新 3。涉及大量代码,但这是用于构建查询的核心代码。

标签: linq-to-sql lambda expression-trees stack-overflow


【解决方案1】:

我最初有怀疑,但现在可以确认。

您正在组合两个具有两个完全不同的参数实例的 lambda。参数实例是不可交换的,即使它们具有相同的名称和相同的类型。它们是不同范围内的有效参数。当您尝试使用错误的参数对象调用其中一个表达式时,就会出现混乱,在这种情况下,会出现堆栈溢出。

您应该做的是创建一个新的参数实例(或重用一个)并重新绑定 lambda 的主体以使用该新参数。我怀疑这会解决这个问题。更进一步,您应该通过重建它们来正确组合这些表达式,而不是将它们作为方法调用修补在一起。我怀疑查询提供者是否会喜欢这些调用。

尝试您的 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,请检查它是否与我们要查找的参数同名。如果是,则返回我们的新参数,否则返回它,因为我们不应该更改它。如果表达式不是参数,它可能是一个包含子表达式并且必须被替换的表达式。

BinaryExpression 将有一个 Left 操作数和一个 Right 操作数,这两个表达式。它们都需要被反弹,因为它们的表达式树的某个地方可能是需要替换的参数。 Update() 方法会将当前表达式替换为具有新子表达式的类似表达式。在这种情况下,我们只想(可能)更新 LeftRight 子表达式。

MethodCallExpressionInvocationExpression 具有相同的想法,但它的树略有不同。它具有Object 表达式(或在调用的情况下为Expression),它表示您要调用的实例(或委托/lambda)。 (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);
}

【讨论】:

  • 嗯。那是个很好的观点。我尝试了这种更改,但得到了相同的结果(StackOverflow)。我认为对 SQL 的翻译处理 And/Or 和 AndAlso/OrElse 的方式相同,因为我已经使用根 And/Or 代码有一段时间了,结果正确(本例除外)。谢谢。
  • @Josh:然后我们必须查看完整的堆栈跟踪。我怀疑提供者会因如此简单的表达而窒息。除非这些属性有副作用,否则您不应使用此表达式导致堆栈溢出。由于您的构建方式,您的表达式构建器很容易引起问题。您可能在其他地方做了不恰当的事情导致溢出。
  • 我似乎无法获得堆栈跟踪,并且在单步执行 .NET Framework 代码时遇到了问题。 (请参阅我的其他帖子:stackoverflow.com/questions/5751678/…)这非常令人沮丧!
  • @Josh:您可以尝试将设置恢复为堆栈溢出时的设置吗?如有必要,恢复默认设置并可能重新启动?关于如何解决这个问题,我无法给你一个明确的答案。如果不访问相同的环境和代码来重现,这种问题很难调试或提供洞察力。
  • 嘿 - 这是一个重现问题的示例项目:cid-0ede5d21bdc5f270.office.live.com/self.aspx/Public/Forums/…
【解决方案2】:

查看您提供的信息后,我有点难过。如果您愿意在黑暗中开玩笑,请尝试以下代码:

using (XYZDataContext context = new XYZDataContext())
{
    var queryableThings = context.Things.AsQueryable();
    var result = queryableThings.Where(expression);
    int count = result.Count();
}

如果这没有透露任何信息,我会开始怀疑 Thing 实体的属性 getter 方法的副作用。也许某些交互会导致递归?

您是否偶然使用了 Mono?

并不是说不可能,但如果这是 LinqToSQL 提供程序中的错误,我会感到非常惊讶。

【讨论】:

  • 谢谢。我尝试了这个并得到了相同的结果。当然,如果我这样做context.Things.ToList().Where(expression).Count(),那么它工作正常。
  • 那我就不知所措了。从您上面所说的看来,当expression 被转换为sql 时似乎发生了异常。这对我来说有点令人震惊。如果我想到什么我会发布它。当您弄清楚时,请在此处添加。祝你好运!
  • 仅供参考:删除 one 的内部 or'd 子句,它工作正常。因此,同时拥有内部 or'd 子句会以某种方式破坏从 LINQ 到 SQL 的转换。
  • 嘿 - 这是一个重现问题的示例项目:cid-0ede5d21bdc5f270.office.live.com/self.aspx/Public/Forums/…
  • 好的。我在我的机器上运行代码并收到溢出异常。一旦有机会深入研究,我会回来报告。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-01-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-05-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多