Ошибка LINQ to SQL (или очень странная особенность) при использовании IQueryable, foreach и нескольких Where - PullRequest
2 голосов
/ 16 ноября 2008

Я столкнулся со сценарием, в котором LINQ to SQL действует очень странно. Я хотел бы знать, если я делаю что-то не так. Но я думаю, что есть реальная возможность, что это ошибка.

Код, вставленный ниже, не является моим настоящим кодом. Это упрощенная версия, которую я создал для этого поста, используя базу данных Northwind.

Немного предыстории: у меня есть метод, который принимает IQueryable из Product и "объект фильтра" (который я опишу через минуту). Он должен запустить некоторые методы расширения «Где» на IQueryable на основе «объекта фильтра», а затем вернуть IQueryable.

Так называемый «объект фильтра» - это System.Collections.Generic.List анонимного типа этой структуры: { column = fieldEnum, id = int }

FieldEnum - это перечисление различных столбцов таблицы Products, которые я, возможно, хотел бы использовать для фильтрации.

Вместо дальнейшего объяснения того, как работает мой код, будет проще, если вы просто посмотрите на него. Это просто следовать.

enum filterType { supplier = 1, category }
public IQueryable<Product> getIQueryableProducts()
{
    NorthwindDataClassesDataContext db = new NorthwindDataClassesDataContext();
    IQueryable<Product> query = db.Products.AsQueryable();

    //this section is just for the example. It creates a Generic List of an Anonymous Type
    //with two objects. In real life I get the same kind of collection, but it isn't hard coded like here
    var filter1 = new { column = filterType.supplier, id = 7 };
    var filter2 = new { column = filterType.category, id = 3 };
    var filterList = (new[] { filter1 }).ToList();
    filterList.Add(filter2);

    foreach(var oFilter in filterList)
    {
        switch (oFilter.column)
        {
            case filterType.supplier:
                query = query.Where(p => p.SupplierID == oFilter.id);
                break;
            case filterType.category:
                query = query.Where(p => p.CategoryID == oFilter.id);
                break;
            default:
                break;
        }
    }
    return query;
}

Итак, вот пример. Допустим, список содержит два элемента этого анонимного типа, { column = fieldEnum.Supplier, id = 7 } и { column = fieldEnum.Category, id = 3}.

После выполнения приведенного выше кода базовый SQL-запрос объекта IQueryable должен содержать:

WHERE SupplierID = 7 AND CategoryID = 3

Но на самом деле, после запуска кода SQL, который будет выполнен, будет

WHERE SupplierID = 3 AND CategoryID = 3

Я попытался определить query как свойство и установить точку останова на сеттере, думая, что смогу понять, что его меняет, когда этого не должно быть. Но якобы все было хорошо. Поэтому вместо этого я просто проверял базовый SQL после каждой команды. Я понял, что первый Where работает нормально, а query остается в порядке (имеется в виду SupplierID = 7) до тех пор, пока цикл foreach не запустится во второй раз. Сразу после того, как oFilter становится вторым элементом анонимного типа, а не первым, SQL-запрос «запрос» меняется на Supplier = 3. Таким образом, то, что должно происходить здесь скрытно, заключается в том, что вместо того, чтобы просто помнить, что Supplier должно равняться 7, LINQ to SQL помнит, что поставщик должен равняться oFilter.id. Но oFilter - это имя отдельного элемента цикла foreach, и после итерации оно означает что-то другое.

Ответы [ 4 ]

6 голосов
/ 16 ноября 2008

Я только взглянул на ваш вопрос, но я на 90% уверен, что вы должны прочитать первый раздел О лямбдах, перехвате и изменчивости (который включает ссылки на 5 подобных вопросов SO) и все станет ясно.

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

Лекарство заключается в том, чтобы вставить новую переменную в цикл foreach, область действия которого - только эта итерация, а не весь цикл:

foreach(var oFilter in filterList)
{
    var filter = oFilter; // add this
    switch (oFilter.column) // this doesn't have to change, but can for consistency
    {
        case filterType.supplier:
            query = query.Where(p => p.SupplierID == filter.id); // use `filter` here
            break;

Теперь каждое замыкание находится над другой filter переменной, которая объявляется заново внутри каждого цикла, и ваш код будет работать как положено.

2 голосов
/ 16 ноября 2008

Работает как задумано. Проблема, с которой вы сталкиваетесь, - это конфликт между лексическим замыканием и изменяемыми переменными .

Что вы, вероятно, хотите сделать, это

foreach(var oFilter in filterList)
{
    var o = oFilter;
    switch (o.column)
    {
        case filterType.supplier:
            query = query.Where(p => p.SupplierID == o.id);
            break;
        case filterType.category:
            query = query.Where(p => p.CategoryID == o.id);
            break;
        default:
            break;
    }
}

При компиляции в IL переменная oFilter объявляется один раз и используется multiply . Вам нужна переменная, объявленная отдельно для каждого использования этой переменной в замыкании, для чего теперь o.

Пока вы это делаете, избавьтесь от этой убогой венгерской нотации: P.

0 голосов
/ 07 января 2010

Я думаю, что это самое ясное объяснение, которое я когда-либо видел: http://blogs.msdn.com/ericlippert/archive/2009/11/12/closing-over-the-loop-variable-considered-harmful.aspx:

По сути, проблема возникает потому, что мы указываем, что цикл foreach является синтаксическим сахаром для

{
    IEnumerator<int> e = ((IEnumerable<int>)values).GetEnumerator();
    try
    {
        int m; // OUTSIDE THE ACTUAL LOOP
        while(e.MoveNext())
        {
            m = (int)(int)e.Current;
            funcs.Add(()=>m);
        }
    }
    finally
    {
        if (e != null) ((IDisposable)e).Dispose();
    }
}

Если мы указали, что расширение было

try
{
    while(e.MoveNext())
    {
        int m; // INSIDE
        m = (int)(int)e.Current;
        funcs.Add(()=>m);
    }

тогда код будет вести себя как положено.

0 голосов
/ 18 ноября 2008

Проблема в том, что вы не добавляете запрос, а заменяете его каждый раз с помощью оператора foreach.

Вы хотите что-то вроде PredicateBuilder - http://www.albahari.com/nutshell/predicatebuilder.aspx

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...