Linq to SQL выбрасывает исключение StackOverflowException - PullRequest
7 голосов
/ 21 апреля 2011

Я выполняю довольно простой запрос, используя 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

Удаление один внутреннегоили пункты, это работает нормально.Таким образом, наличие как внутренних, так и условных предложений нарушает перевод из 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 такой же, но сВыражение. И вместо этого):

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

Возможно, оно будет осуждено, но вот пример , который воспроизводит эту проблему .Мне действительно нужно выяснить, что здесь происходит.

Ответы [ 2 ]

7 голосов
/ 22 апреля 2011

У меня изначально были подозрения, но теперь я могу подтвердить это.

Вы объединяете две лямбды, которые имеют два совершенно разных экземпляра своих параметров. Экземпляры параметров не могут быть заменены, даже если они имеют одинаковые имена и типы. Они являются эффективными параметрами в разных областях. Когда вы попытались вызвать одно из выражений с неверным параметром объекта, в этом случае возникает хаос, в этом случае переполнение стека.

Что вам следует сделать, это создать новый экземпляр параметра (или повторно использовать его) и заново привязать тела ваших лямбд, чтобы использовать этот новый параметр. Я подозреваю, что это исправит. И чтобы пойти дальше, вы должны правильно объединить эти выражения, перестроив их, а не соединяя их вместе как вызовы методов. Я сомневаюсь, что поставщикам запросов это понравится как вызовам.

Попробуйте выполнить эту реализацию ваших 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() методе, который обновляет параметры в пределах всего дерева выражений.

Чтобы дополнительно проиллюстрировать, иллюстрацию, которая поможет визуализировать, как выглядит дерево и что меняется. Обратите внимание, что поскольку здесь происходят замены, большинство поддеревьев будут новыми экземплярами.

[illustration


Теперь вот что-то, чего я не знал, было доступно, 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);
}
0 голосов
/ 21 апреля 2011

После просмотра предоставленной вами информации я немного озадачен.Если вы хотите испортить снимок в темноте, попробуйте следующий код:

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

Если это ничего не выявит, я бы заподозрил побочные эффекты объекта получения объекта Thingметоды.Может быть, какое-то взаимодействие приводит к рекурсии?

Вы используете Mono случайно?

Не то чтобы это невозможно, но я был бы очень удивлен, если это ошибка в поставщике LinqToSQL.

...