2009-05-06 4 views
2

Je viens d'avoir l'expérience de débogage la plus étrange depuis très longtemps. C'est un peu embarrassant d'admettre, mais cela m'amène à croire que ma requête Linq produit PLUS de résultats lors de l'ajout d'une clause Where supplémentaire.Pourquoi ma clause Linq Where produit-elle plus de résultats au lieu de moins?

Je sais que ce n'est pas possible, donc je l'ai refactorisé ma fonction incriminée ainsi que le test unitaire appartenant dans ceci:

[Test] 
public void LoadUserBySearchString() 
{ 
    //Setup 
    var AllUsers = new List<User> 
         { 
          new User 
           { 
            FirstName = "Luke", 
            LastName = "Skywalker", 
            Email = "[email protected]" 
           }, 
          new User 
           { 
            FirstName = "Leia", 
            LastName = "Skywalker", 
            Email = "[email protected]" 
           } 
         }; 


    //Execution 
    List<User> SearchResults = LoadUserBySearchString("princess", AllUsers.AsQueryable()); 
    List<User> SearchResults2 = LoadUserBySearchString("princess Skywalker", AllUsers.AsQueryable()); 

    //Assertion 
    Assert.AreEqual(1, SearchResults.Count); //test passed! 
    Assert.AreEqual(1, SearchResults2.Count); //test failed! got 2 instead of 1 User??? 
} 


//search CustID, fname, lname, email for substring(s) 
public List<User> LoadUserBySearchString(string SearchString, IQueryable<User> AllUsers) 
{ 
    IQueryable<User> Result = AllUsers; 
    //split into substrings and apply each substring as additional search criterium 
    foreach (string SubString in Regex.Split(SearchString, " ")) 
    {    
     int SubStringAsInteger = -1; 
     if (SubString.IsInteger()) 
     { 
      SubStringAsInteger = Convert.ToInt32(SubString); 
     } 

     if (SubString != null && SubString.Length > 0) 
     { 
      Result = Result.Where(c => (c.FirstName.Contains(SubString) 
             || c.LastName.Contains(SubString) 
             || c.Email.Contains(SubString) 
             || (c.ID == SubStringAsInteger) 
             )); 
     } 
    } 
    return Result.ToList(); 
} 

Je déboguée la fonction LoadUserBySearchString et a affirmé que le second appel à la fonction produit en fait une requête linq avec deux clauses where au lieu d'une. Il semble donc que la clause where supplémentaire augmente le nombre de résultats.

Ce qui est encore plus bizarre, la fonction LoadUserBySearchString fonctionne très bien lorsque je la teste à la main (avec de vrais utilisateurs de la base de données). Il montre seulement ce comportement bizarre lors de l'exécution du test unitaire. Je suppose que j'ai juste besoin de dormir (ou même de longues vacances). Si quelqu'un pouvait m'aider à faire la lumière sur ce sujet, je pourrais arrêter d'interroger ma santé mentale et retourner au travail.

Merci,

Adrian

Modifier (pour clarifier plusieurs réponses que je vais à ce jour): Je sais qu'il ressemble à c'est la ou clause, mais unfortuantely ce n'est pas aussi simple que cela. LoadUserBySearchString divise la chaîne de recherche en plusieurs chaînes et attache une clause Where à chacune d'entre elles. "Skywalker" correspond à la fois à Luke et à Leia, mais "princesse" ne correspond qu'à Leia.

Ceci est la requête Linq pour la chaîne de recherche "princesse":

+  Result {System.Collections.Generic.List`1[TestProject.Models.User].Where(c => (((c.FirstName.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString) || c.LastName.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString)) || c.Email.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString)) || (c.ID = value(TestProject.Controllers.SearchController+<>c__DisplayClass3).SubStringAsInteger)))} System.Linq.IQueryable<TestProject.Models.User> {System.Linq.EnumerableQuery<TestProject.Models.User>} 

Et c'est la clause Linq pour la chaîne de recherche "princesse Skywalker"

+  Result {System.Collections.Generic.List`1[TestProject.Models.User].Where(c => (((c.FirstName.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString) || c.LastName.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString)) || c.Email.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString)) || (c.ID = value(TestProject.Controllers.SearchController+<>c__DisplayClass3).SubStringAsInteger))).Where(c => (((c.FirstName.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString) || c.LastName.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString)) || c.Email.Contains(value(TestProject.Controllers.SearchController+<>c__DisplayClass1).SubString)) || (c.ID = value(TestProject.Controllers.SearchController+<>c__DisplayClass3).SubStringAsInteger)))} System.Linq.IQueryable<TestProject.Models.User> {System.Linq.EnumerableQuery<TestProject.Models.User>} 

comme ci-dessus, juste avec une clause where supplémentaire.

Répondre

6

Ceci est un joli petit gotcha.

Ce qui se passe, c'est qu'en raison des méthodes anonymes, et de l'exécution différée, vous ne filtrez pas sur "princess". Au lieu de cela, vous construisez un filtre qui filtrera le contenu de la variable subString. Mais, vous modifiez ensuite cette variable et construisez un autre filtre, qui utilise à nouveau la même variable.

Fondamentalement, c'est ce que vous allez exécuter, sous une forme simplifiée:

Where(...contains(SubString)).Where(...contains(SubString)) 

, vous êtes en train de filtrer uniquement sur le dernier mot, qui existe dans les deux, tout simplement parce que le temps, ces filtres sont effectivement appliqué, il n'y a plus qu'une seule valeur SubString, la dernière.

Si vous modifiez le code afin que vous capturez les variables à l'intérieur du sous-chaîne étendue de la boucle, ça va marcher:

if (SubString != null && SubString.Length > 0) 
{ 
    String captured = SubString; 
    Int32 capturedId = SubStringAsInteger; 
    Result = Result.Where(c => (c.FirstName.Contains(captured) 
           || c.LastName.Contains(captured) 
           || c.Email.Contains(captured) 
           || (c.ID == capturedId) 
           )); 
} 
+0

Merci beaucoup! Vous avez fait ma journée :-) –

+0

+1, en utilisant une variable locale aide généralement à résoudre les problèmes de fermeture. Pour en savoir plus sur l'utilisation des fermetures dans LINQ: http://diditwith.net/2007/09/25/LINQClosuresMayBeHazardousToYourHealth.aspx – Lucas

1

Votre algorithme revient à "sélectionner les enregistrements qui correspondent à l'un des mots de la chaîne de recherche".

Ceci est dû à l'exécution différée. La requête n'est pas réellement effectuée jusqu'à ce que vous appeliez le .ToList(). Si vous déplacez le .ToList() dans la boucle, vous obtiendrez le comportement que vous voulez.

+0

Vous avez tort, voir la réponse de lassevk. – Samuel

+3

Prendre soin d'expliquer quelle partie de ma réponse est fausse? Je suis correct sur les deux points - il est causé par une exécution différée, et faire un .ToList() dans la boucle chaque fois donnerait la bonne réponse. –

+0

Votre tout premier énoncé est faux. Lisez son code et vous verrez pourquoi. Et votre suggestion ne fonctionne que s'il appelle ToList(). AsQueryable() à l'intérieur de chaque boucle puisqu'il a besoin de IQueryable . Et que se passe-t-il si le IQueryable est lent? Vous avez maintenant rendu sa requête plus lente. – Samuel