web-dev-qa-db-fra.com

LINQ where clause avec expression lambda ayant OR clauses et valeurs nulles retournant des résultats incomplets

le problème en bref

nous avons une expression lambda utilisée dans la clause Where, qui ne renvoie pas le résultat "attendu".

résumé rapide

dans l'objet analysisObjectRepository, certains objets contiennent également la relation parent dans une propriété nommée Parent. nous interrogeons ce analysisObjectRepository pour renvoyer certains objets.

détail

ce que le code ci-dessous est censé faire, c'est retourner la racine, les premiers enfants (enfants immédiats) et les petits-enfants d'un objet spécifique contenant la valeur ID.

dans le code ci-dessous, le bon sens dit que tous les résultats qui rendent l'une des 3 conditions OR vraies séparées doivent être retournés comme dans les résultats.

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

mais le code ci-dessus ne renvoie que les enfants et petits-enfants, sans renvoyer les objets racine (avec une valeur parent nulle) qui rendent le

x.ID == packageId

condition vraie.

seuls les objets qui font le second

x.Parent.ID == packageId

et troisième

x.Parent.Parent.ID == packageId

les clauses sont retournées.

Si nous écrivons uniquement le code pour renvoyer l'objet racine avec le code ci-dessous, il est renvoyé, nous sommes donc totalement sûrs que analysisObjectRepository contient tous les objets

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

Cependant, lorsque nous le réécrivons en tant que délégué, nous obtenons le résultat attendu, en retournant tous les objets attendus.

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();

question

Manquons-nous quelque chose dans l'expression lambda? il s'agit d'une condition très simple en 3 parties OR et nous pensons que tout objet qui rend l'une des trois conditions vraies doit être retourné. nous soupçonnons que l'objet racine ayant une valeur parent nulle pourrait provoquer un problème mais n'a pas pu le comprendre exactement.

toute aide est la bienvenue.

12
Cihan Kurt

Votre deuxième délégué n'est pas une réécriture du premier au format délégué anonyme (plutôt que lambda). Regardez vos conditions.

Premier:

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

Seconde:

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

L'appel au lambda lèverait une exception pour tout x où l'ID ne correspond pas et où le parent est nul ou ne correspond pas et le grand-parent est nul. Copiez les contrôles nuls dans le lambda et cela devrait fonctionner correctement.

Modifier après le commentaire de la question

Si votre objet d'origine n'est pas un List<T>, Nous n'avons aucun moyen de savoir quel est le type de retour de FindAll(), et si cela implémente ou non l'interface IQueryable. Si tel est le cas, cela explique probablement l'écart. Étant donné que les lambdas peuvent être convertis au moment de la compilation en un Expression<Func<T>> mais que les délégués anonymes ne peuvent pas , vous utilisez peut-être l'implémentation de IQueryable lors de l'utilisation de la version lambda mais LINQ-to-Objects lors de l'utilisation de la version déléguée anonyme.

Cela expliquerait également pourquoi votre lambda ne provoque pas de NullReferenceException. Si vous deviez passer cette expression lambda à quelque chose qui implémente IEnumerable<T> Mais pas IQueryable<T>, Évaluation à l'exécution du lambda (qui n'est pas différent des autres méthodes, anonymes ou non) lancerait un NullReferenceException la première fois qu'il rencontrait un objet où ID n'était pas égal à la cible et le parent ou grand-parent était nul.

Ajouté le 16/03/2011 08:29 EDT

Prenons l'exemple simple suivant:

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");

Ces deux méthodes produisent des résultats entièrement différents.

La première requête est la version simple. La méthode anonyme entraîne un délégué qui est ensuite passé à la méthode d'extension IEnumerable<MyObject>.Where, Où tout le contenu de source sera vérifié (manuellement en mémoire à l'aide du code compilé ordinaire) par rapport à votre délégué. En d'autres termes, si vous connaissez les blocs d'itérateur en C #, c'est quelque chose comme faire ceci:

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

Le point saillant ici est que vous effectuez réellement votre filtrage en mémoire côté client. Par exemple, si votre source était un ORM SQL, il n'y aurait pas de clause WHERE dans la requête; l'ensemble des résultats serait ramené au client et filtré là-bas .

La deuxième requête, qui utilise une expression lambda, est convertie en Expression<Func<MyObject, bool>> Et utilise la méthode d'extension IQueryable<MyObject>.Where(). Il en résulte un objet qui est également tapé comme IQueryable<MyObject>. Tout cela fonctionne en passant ensuite l'expression au fournisseur sous-jacent. C'est pourquoi vous n'obtenez pas de NullReferenceException. Il appartient entièrement au fournisseur de requêtes de traduire l'expression (qui, au lieu d'être une fonction compilée réelle qu'elle peut simplement appeler, est une représentation de la logique de l'expression utilisant des objets) en quelque chose qu'il peut utiliser.

Un moyen facile de voir la distinction (ou, au moins, qu'il y a est ) une distinction, serait d'appeler AsEnumerable() avant votre appel à Where dans la version lambda. Cela forcera votre code à utiliser LINQ-to-Objects (ce qui signifie qu'il fonctionne sur IEnumerable<T> Comme la version déléguée anonyme, et non IQueryable<T> Comme la version lambda le fait actuellement), et vous obtiendrez le exceptions comme prévu.

Version TL; DR

Le long et le court, c'est que votre expression lambda est traduite en une sorte de requête sur votre source de données, tandis que la version de la méthode anonyme évalue l'ensemble source de données en mémoire. Quoi que fasse la traduction de votre lambda en une requête ne représente pas la logique que vous attendez, c'est pourquoi il ne produit pas les résultats que vous attendez.

13
Adam Robinson

Essayez d'écrire le lambda avec les mêmes conditions que le délégué. comme ça:

  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();
6
Jean-Louis Mbaka

Vous vérifiez les propriétés Parent pour null dans votre délégué. La même chose devrait également fonctionner avec les expressions lambda.

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();
2
mgronber