Comparar tipos anuláveis ​​em Linq to Sql

Eu tenho uma entidade de categoria que possui um campo Nullable ParentId. Quando o método abaixo está em execução e o categoryId é nulo, o resultado parece nulo, no entanto, há categorias que possuem valor ParentId nulo.

Qual é o problema aqui, o que estou perdendo?

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

By the way, quando eu mudar a condição para (c.ParentId == null), o resultado parece normal.

A primeira coisa a fazer é colocar em log, para ver o que o TSQL foi gerado; por exemplo:

 ctx.Log = Console.Out; 

O LINQ-to-SQL parece tratar os valores nulos um pouco inconsistentemente (dependendo do valor 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(); } 

Então, tudo o que posso sugerir é usar o formulário principal com nulos!

ou seja

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

Atualização – eu consegui trabalhar “corretamente” usando uma Expression personalizada:

  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 Where( this IQueryable source, Expression> 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>(body, param); return source.Where(lambda); } 

Outro jeito:

 Where object.Equals(c.ParentId, categoryId) 

ou

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

Você precisa usar o operador Equals:

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

Equals fot nullable types retorna true se:

  • A propriedade HasValue é falsa e o outro parâmetro é nulo. Ou seja, dois valores nulos são iguais por definição.
  • A propriedade HasValue é true e o valor retornado pela propriedade Value é igual ao outro parâmetro.

e retorna falso se:

  • A propriedade HasValue para a estrutura Nullable atual é true e o outro parâmetro é null.
  • A propriedade HasValue para a estrutura Nullable atual é false e o outro parâmetro não é nulo.
  • A propriedade HasValue para a estrutura Nullable atual é true e o valor retornado pela propriedade Value não é igual ao outro parâmetro.

Mais informações aqui Método <.T> .Equals Nullable

Meu palpite é que isso se deve a um atributo bastante comum dos SGBDs – só porque duas coisas são nulas, não significa que sejam iguais.

Para elaborar um pouco, tente executar estas duas consultas:

 SELECT * FROM TABLE WHERE field = NULL SELECT * FROM TABLE WHERE field IS NULL 

A razão para a construção “IS NULL” é que no mundo DBMS, NULL! = NULL desde que o significado de NULL é que o valor é indefinido. Como NULL significa indefinido, você não pode dizer que dois valores nulos são iguais, pois, por definição, você não sabe o que são.

Quando você explicitamente verificar para “campo == NULL”, LINQ provavelmente converte isso para “campo IS NULL”. Mas quando você usa uma variável, estou supondo que o LINQ não faz automaticamente essa conversão.

Aqui está uma postagem no fórum do MSDN com mais informações sobre esse problema.

Parece que um bom “truque” é mudar o seu lambda para ficar assim:

 c => c.ParentId.Equals(categoryId) 

Que tal algo mais simples assim?

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

Ou você pode simplesmente usar isso. Ele também será traduzido para uma consulta sql mais agradável

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

Linq to Entities suporta Null Coelescing (??) então apenas converta o null imediatamente para um valor padrão.

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