Предложение LINQ where с лямбда-выражением с предложениями OR и нулевыми значениями, возвращающими неполные результаты

короткая проблема

У нас есть лямбда-выражение, используемое в предложении Where, которое не возвращает ожидаемый результат.

краткое резюме

в объекте analysisObjectRepository есть определенные объекты, которые также содержат родительское отношение в свойстве с именем Parent. мы запрашиваем этот анализObjectRepository для возврата некоторых объектов.

подробнее

то, что должен сделать следующий код: возврат корня, первых детей (непосредственных детей) и внуков определенного объекта, содержащего значение ID.

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

List<AnalysisObject> analysisObjects = 
    analysisObjectRepository
        .FindAll()
        .Where(x => x.ID               == packageId ||
                    x.Parent.ID        == packageId || 
                    x.Parent.Parent.ID == packageId)
        .ToList();

но приведенный выше код возвращает только детей и внуков, не возвращая корневые объекты (с нулевым родительским значением), которые делают

x.ID == packageId

условие true.

только объекты, которые делают второй

x.Parent.ID == packageId

и третий

x.Parent.Parent.ID == packageId

возвращаются.

Если мы только напишем код для возврата корневого объекта с приведенным ниже кодом, он будет возвращен, поэтому мы полностью уверены, что в анализе objectObjectRepository содержатся все объекты

List<AnalysisObject> analysisObjects = 
    analysisObjectRepository
        .FindAll()
        .Where(x => x.ID == packageId )
        .ToList();

Однако, когда мы переписываем его как делегата, получаем ожидаемый результат, возвращая все ожидаемые объекты.

List<AnalysisObject> analysisObjects = 
    analysisObjectRepository
        .FindAll()
        .Where(delegate(AnalysisObject x) 
        { 
            return 
              (x.ID == packageId) || 
              (x.Parent != null && x.Parent.ID == packageId) || 
                  (x.Parent != null && 
                   x.Parent.Parent != null && 
                   x.Parent.Parent.ID == packageId); })
        .ToList();

Вопрос

Мы что-то теряем в выражении лямбда? это действительно простое условие 3 части ИЛИ, и мы думаем, что любой объект, который делает любое из трех условий истинным, должен быть возвращен. мы предположили, что корневой объект, имеющий нулевое родительское значение, может вызвать проблему, но не мог точно понять его.

любая помощь будет большой.

Ответ 1

Ваш второй делегат не является переписыванием первого в анонимном делетете (а не лямбда) формате. Посмотрите на свои условия.

Во-первых:

x.ID == packageId || x.Parent.ID == packageId || x.Parent.Parent.ID == packageId

Во-вторых:

(x.ID == packageId) || (x.Parent != null && x.Parent.ID == packageId) || 
(x.Parent != null && x.Parent.Parent != null && x.Parent.Parent.ID == packageId)

При вызове лямбда будет выведено исключение для любого x, где идентификатор не совпадает, и либо родительский объект равен null, либо не соответствует, а grandparent - null. Скопируйте нулевые проверки в лямбда и он должен работать правильно.

Изменить после комментария к вопросу

Если ваш исходный объект не является List<T>, тогда мы не можем узнать, что такое тип возврата FindAll(), и реализует ли он интерфейс IQueryable. Если да, то это, вероятно, объясняет несоответствие. Поскольку lambdas можно преобразовать во время компиляции в Expression<Func<T>>, но анонимные делегаты не могут, тогда вы можете использовать реализацию IQueryable при использовании лямбда-версии, но LINQ-to-Objects при использовании анонимной версии делегата.

Это также объясняет, почему ваша лямбда не вызывает NullReferenceException. Если вы передадите это выражение лямбда тому, что реализует IEnumerable<T>, но не IQueryable<T>, оценка времени выполнения лямбда (которая ничем не отличается от других методов, анонимная или нет) будет бросать NullReferenceException в первый раз, когда она столкнулась объект, в котором ID не был равен цели, а родительский или дед-барабан был нулевым.

Добавлен 3/16/2011 8:29 утра EDT

Рассмотрим следующий простой пример:

IQueryable<MyObject> source = ...; // some object that implements IQueryable<MyObject>

var anonymousMethod =  source.Where(delegate(MyObject o) { return o.Name == "Adam"; });    
var expressionLambda = source.Where(o => o.Name == "Adam");

Эти два метода дают совершенно разные результаты.

Первый запрос - это простая версия. Анонимный метод приводит к тому, что делегат затем передается методу расширения IEnumerable<MyObject>.Where, где все содержимое source будет проверено (вручную в памяти с использованием обычного скомпилированного кода) против вашего делегата. Другими словами, если вы знакомы с итераторными блоками в С#, то что-то вроде этого:

public IEnumerable<MyObject> MyWhere(IEnumerable<MyObject> dataSource, Func<MyObject, bool> predicate)
{
    foreach(MyObject item in dataSource)
    {
        if(predicate(item)) yield return item;
    }
}

Главное, что вы на самом деле выполняете фильтрацию в памяти на стороне клиента. Например, если ваш источник был SQL ORM, в запросе не было бы предложения WHERE; весь результирующий набор будет возвращен клиенту и отфильтрован там.

Второй запрос, который использует лямбда-выражение, преобразуется в Expression<Func<MyObject, bool>> и использует метод расширения IQueryable<MyObject>.Where(). Это приводит к тому, что объект также напечатан как IQueryable<MyObject>. Все это работает, передавая это выражение базовому провайдеру. Вот почему вы не получаете NullReferenceException. Это полностью зависит от поставщика запроса, как преобразовать выражение (которое вместо того, чтобы быть фактически скомпилированной функцией, которую он может просто вызвать, представляет собой представление логики выражения с использованием объектов) во что-то, что он может использовать.

Простым способом увидеть различие (или, по крайней мере, это есть) было бы различие, заключалось бы в вызове AsEnumerable() перед вашим вызовом WHERE в лямбда-версии. Это заставит ваш код использовать LINQ-to-Objects (это означает, что он работает на IEnumerable<T>, как анонимная версия делегата, а не IQueryable<T>, как в настоящее время версия лямбда), и вы получите исключения, как ожидалось.

TL; версия DR

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

Ответ 2

Попробуйте наложить лямбду на те же условия, что и делегат. например:

  List<AnalysisObject> analysisObjects = 
    analysisObjectRepository.FindAll().Where(
    (x => 
       (x.ID == packageId)
    || (x.Parent != null && x.Parent.ID == packageId)
    || (x.Parent != null && x.Parent.Parent != null && x.Parent.Parent.ID == packageId)
    ).ToList();

Ответ 3

Вы проверяете свойства Parent для null в своем делетете. То же самое должно работать и с лямбда-выражениями.

List<AnalysisObject> analysisObjects = analysisObjectRepository
        .FindAll()
        .Where(x => 
            (x.ID == packageId) || 
            (x.Parent != null &&
                (x.Parent.ID == packageId || 
                (x.Parent.Parent != null && x.Parent.Parent.ID == packageId)))
        .ToList();