Сравнение типов с нулевым значением в Linq to Sql

У меня есть объект Category, у которого есть поле Nullable ParentId. Когда выполняется нижеприведенный метод, а categoryId равен null, результат кажется нулевым, однако существуют категории с нулевым значением ParentId.

В чем проблема, что мне не хватает?

public IEnumerable<ICategory> GetSubCategories(long? categoryId)
{
    var subCategories = this.Repository.Categories.Where(c => c.ParentId == categoryId)
        .ToList().Cast<ICategory>();

    return subCategories;
}

Кстати, когда я изменяю условие на (c.ParentId == null), результат кажется нормальным.

Ответ 1

Первое, что нужно сделать, это включить регистрацию, чтобы узнать, какой TSQL был сгенерирован; например:

ctx.Log = Console.Out;

LINQ-to-SQL, по-видимому, обрабатывает nulls немного непоследовательно (в зависимости от значения literal vs):

using(var ctx = new DataClasses2DataContext())
{
    ctx.Log = Console.Out;
    int? mgr = (int?)null; // redundant int? for comparison...
    // 23 rows:
    var bosses1 = ctx.Employees.Where(x => x.ReportsTo == (int?)null).ToList();
    // 0 rows:
    var bosses2 = ctx.Employees.Where(x => x.ReportsTo == mgr).ToList();
}

Итак, все, что я могу предложить, это использовать верхнюю форму с нулями!

то есть.

Expression<Func<Category,bool>> predicate;
if(categoryId == null) {
    predicate = c=>c.ParentId == null;
} else {
    predicate = c=>c.ParentId == categoryId;
}
var subCategories = this.Repository.Categories
           .Where(predicate).ToList().Cast<ICategory>();

Обновление - я получил его "правильно", используя пользовательский Expression:

    static void Main()
    {
        ShowEmps(29); // 4 rows
        ShowEmps(null); // 23 rows
    }
    static void ShowEmps(int? manager)
    {
        using (var ctx = new DataClasses2DataContext())
        {
            ctx.Log = Console.Out;
            var emps = ctx.Employees.Where(x => x.ReportsTo, manager).ToList();
            Console.WriteLine(emps.Count);
        }
    }
    static IQueryable<T> Where<T, TValue>(
        this IQueryable<T> source,
        Expression<Func<T, TValue?>> selector,
        TValue? value) where TValue : struct
    {
        var param = Expression.Parameter(typeof (T), "x");
        var member = Expression.Invoke(selector, param);
        var body = Expression.Equal(
                member, Expression.Constant(value, typeof (TValue?)));
        var lambda = Expression.Lambda<Func<T,bool>>(body, param);
        return source.Where(lambda);
    }

Ответ 2

Другой способ:

Where object.Equals(c.ParentId, categoryId)

или

Where (categoryId == null ? c.ParentId == null : c.ParentId == categoryId)

Ответ 3

Вам нужно использовать оператор Equals:

 var subCategories = this.Repository.Categories.Where(c => c.ParentId.Equals(categoryId))
        .ToList().Cast<ICategory>();

Equals fot nullable возвращает true, если:

  • Свойство HasValue имеет значение false, а другой параметр - null. То есть, два нулевых значения по определению равны.
  • Свойство HasValue равно true, а значение, возвращаемое свойством Value, равно другому параметру.

и возвращает false, если:

  • Свойство HasValue для текущей структуры Nullable - true, а другой параметр - null.
  • Свойство HasValue для текущей структуры Nullable является ложным, а другой параметр не является нулевым.
  • Свойство HasValue для текущей структуры Nullable истинно, а значение, возвращаемое свойством Value, не равно другому параметру.

Подробнее здесь Метод Nullable <.T > .Equals

Ответ 4

Я предполагаю, что это связано с довольно распространенным атрибутом СУБД. Просто потому, что две вещи являются нулевыми, это не значит, что они равны.

Чтобы разработать бит, попробуйте выполнить эти два запроса:

SELECT * FROM TABLE WHERE field = NULL

SELECT * FROM TABLE WHERE field IS NULL

Причина конструкции "IS NULL" заключается в том, что в мире СУБД NULL!= NULL, поскольку значение NULL заключается в том, что значение undefined. Поскольку NULL означает undefined, вы не можете сказать, что два нулевых значения равны, поскольку по определению вы не знаете, что они собой представляют.

Когда вы явно проверяете "field == NULL", LINQ, вероятно, преобразует его в "поле IS NULL". Но когда вы используете переменную, я предполагаю, что LINQ автоматически не выполняет это преобразование.

Здесь сообщение форума MSDN с дополнительной информацией об этой проблеме.

Похоже, хороший "чит" - это изменить вашу лямбду, чтобы выглядеть так:

c => c.ParentId.Equals(categoryId)

Ответ 5

Как насчет чего-то более простого?

public IEnumerable<ICategory> GetSubCategories(long? categoryId)
{
    var subCategories = this.Repository.Categories.Where(c => (!categoryId.HasValue && c.ParentId == null) || c.ParentId == categoryId)
        .ToList().Cast<ICategory>();

    return subCategories;
}

Ответ 6

Или вы можете просто использовать это. Он также переведёт на более удобный SQL-запрос

Where((!categoryId.hasValue && !c.ParentId.HasValue) || c.ParentId == categoryId)

Ответ 7

Linq to Entities поддерживает Null Coelescing (??), поэтому просто преобразуйте значение null на лету в значение по умолчанию.

Where(c => c.ParentId == categoryId ?? 0)