Comment feriez-vous une requête «pas dans» avec LINQ?

307

J'ai deux collections qui ont des propriétés Emaildans les deux collections. J'ai besoin d'obtenir une liste des éléments de la première liste où Emailn'existe pas dans la deuxième liste. Avec SQL, j'utiliserais simplement "pas dans", mais je ne connais pas l'équivalent dans LINQ. Comment cela se fait-il?

Jusqu'à présent, j'ai une jointure, comme ...

var matches = from item1 in list1
join item2 in list2 on item1.Email equals item2.Email
select new { Email = list1.Email };

Mais je ne peux pas rejoindre car j'ai besoin de la différence et la jointure échouerait. J'ai besoin d'une manière d'utiliser Contains ou Exists, je crois. Je n'ai tout simplement pas encore trouvé d'exemple pour le faire.

Brennan
la source
3
Veuillez noter que la réponse d'Echostorm produit un code beaucoup plus clair à lire que celui de Robert
Nathan Koop

Réponses:

302

Je ne sais pas si ça va t'aider mais ..

NorthwindDataContext dc = new NorthwindDataContext();    
dc.Log = Console.Out;

var query =    
    from c in dc.Customers    
    where !(from o in dc.Orders    
            select o.CustomerID)    
           .Contains(c.CustomerID)    
    select c;

foreach (var c in query) Console.WriteLine( c );

de la clause NOT IN dans LINQ to SQL par Marco Russo

Robert Rouse
la source
Mais j'utilise linq pour les entités, donc j'obtiens "seuls les types primitifs peuvent être utilisés en erreur". Y a-t-il une solution de contournement...? en plus d'itérer manuellement et de trouver la liste.
Novice
13
Cela fonctionne bien pour moi avec LINQ to Entities. Le SQL devient une requête WHERE NOT EXISTS (sous-requête). Peut-être y avait-il une mise à jour qui corrige cela?
scottheckel
2
Je pense que les nouvelles versions d'EF prennent en charge .Contient, plus cette question ne marque pas EF (version) ou LinqToSQL .. il peut donc être nécessaire d'étendre la question et la réponse ici ..
Brett Caswell
4
@Robert Rouse - Le lien vers The Not in cluse in linq to sql ne fonctionne plus. Juste un fyi.
JonH
Le lien fourni mène à un site signalé comme contenant des logiciels malveillants.
mikesigs
334

Vous voulez l'opérateur Except.

var answer = list1.Except(list2);

Meilleure explication ici: https://docs.microsoft.com/archive/blogs/charlie/linq-farm-more-on-set-operators

REMARQUE: cette technique fonctionne mieux pour les types primitifs uniquement, car vous devez implémenter un IEqualityComparer pour utiliser la Exceptméthode avec des types complexes.

Echostorm
la source
7
Utiliser Except: Si vous travaillez avec des listes de types complexes, vous devez implémenter un IEqualityComparer <MyComlplexType>, ce qui le rend pas si agréable
sakito
4
Vous ne devez mettre en œuvre IEqualityComparer <T> si vous voulez juste de comparer l' égalité de référence ou si vous avez T.Equals () redéfinie et T.GetHashCode (). Si vous n'implémentez pas IEqualityComparer <T>, EqualityComparer <T> .Default sera utilisé.
piedar
2
@Echostorm (et les autres personnes qui lisent), si vous effectuez un objet Select to Anonymous, le HashCode sera déterminé par les valeurs de propriété; list1.Select(item => new { Property1 = item.Property1, Property2 = item.Property2 }).Except(list2.Select( item => new { Property1 = item.Property1, Property2 = item.Property2 }));ceci est particulièrement utile lorsque vous déterminez l'égalité en évaluant uniquement un ensemble de valeurs de type complexe.
Brett Caswell
3
En fait, quelqu'un a souligné ci-dessous, et je pense à juste titre, qu'il ne serait pas nécessaire d'implémenter IEquatityComparor<T,T>ou de remplacer des méthodes de comparaison d'objets dans un LinqToSqlscénario; pour, la requête sera représentée comme / compilée en / exprimée en SQL; ainsi les valeurs seront vérifiées, pas la référence d'objet.
Brett Caswell
2
En utilisant le, exceptj'ai pu accélérer une requête LINQ de 8 à 10 secondes à une demi-seconde
Michael Kniskern
61

Pour les personnes qui commencent par un groupe d'objets en mémoire et interrogent une base de données, j'ai trouvé que c'était la meilleure façon de procéder:

var itemIds = inMemoryList.Select(x => x.Id).ToArray();
var otherObjects = context.ItemList.Where(x => !itemIds.Contains(x.Id));

Cela produit une belle WHERE ... IN (...)clause en SQL.

StriplingWarrior
la source
1
en fait, vous pouvez le faire en 3.5
George Silva
59

les éléments de la première liste où l'e-mail n'existe pas dans la deuxième liste.

from item1 in List1
where !(list2.Any(item2 => item2.Email == item1.Email))
select item1;
Amy B
la source
16

Vous pouvez utiliser une combinaison de Où et de Tout pour ne pas trouver dans:

var NotInRecord =list1.Where(p => !list2.Any(p2 => p2.Email  == p.Email));
DevT
la source
8

Vous pouvez regrouper les deux collections dans deux listes différentes, par exemple list1 et list2.

Alors écrivez

list1.RemoveAll(Item => list2.Contains(Item));

Cela fonctionnera.

Chintan Udeshi
la source
3
Bien mais a pour effet secondaire de supprimer des éléments de la liste.
Tarik
7

Dans le cas où l'on utilise ADO.NET Entity Framework , la solution d'EchoStorm fonctionne également parfaitement. Mais il m'a fallu quelques minutes pour m'enrouler autour de la tête. En supposant que vous avez un contexte de base de données, dc, et que vous souhaitez trouver des lignes dans le tableau x non liées dans le tableau y, la réponse complète de la réponse ressemble à:

var linked =
  from x in dc.X
  from y in dc.Y
  where x.MyProperty == y.MyProperty
  select x;
var notLinked =
  dc.X.Except(linked);

En réponse au commentaire d'Andy, oui, on peut avoir deux from dans une requête LINQ. Voici un exemple de travail complet, utilisant des listes. Chaque classe, Foo et Bar, a un identifiant. Foo a une référence de "clé étrangère" à Bar via Foo.BarId. Le programme sélectionne tous les Foo non liés à une barre correspondante.

class Program
{
    static void Main(string[] args)
    {
        // Creates some foos
        List<Foo> fooList = new List<Foo>();
        fooList.Add(new Foo { Id = 1, BarId = 11 });
        fooList.Add(new Foo { Id = 2, BarId = 12 });
        fooList.Add(new Foo { Id = 3, BarId = 13 });
        fooList.Add(new Foo { Id = 4, BarId = 14 });
        fooList.Add(new Foo { Id = 5, BarId = -1 });
        fooList.Add(new Foo { Id = 6, BarId = -1 });
        fooList.Add(new Foo { Id = 7, BarId = -1 });

        // Create some bars
        List<Bar> barList = new List<Bar>();
        barList.Add(new Bar { Id = 11 });
        barList.Add(new Bar { Id = 12 });
        barList.Add(new Bar { Id = 13 });
        barList.Add(new Bar { Id = 14 });
        barList.Add(new Bar { Id = 15 });
        barList.Add(new Bar { Id = 16 });
        barList.Add(new Bar { Id = 17 });

        var linked = from foo in fooList
                     from bar in barList
                     where foo.BarId == bar.Id
                     select foo;
        var notLinked = fooList.Except(linked);
        foreach (Foo item in notLinked)
        {
            Console.WriteLine(
                String.Format(
                "Foo.Id: {0} | Bar.Id: {1}",
                item.Id, item.BarId));
        }
        Console.WriteLine("Any key to continue...");
        Console.ReadKey();
    }
}

class Foo
{
    public int Id { get; set; }
    public int BarId { get; set; }
}

class Bar
{
    public int Id { get; set; }
}
Brett
la source
est-ce que deux froms travaillent dans LINQ? ce serait utile.
Andy
Andy: Oui, voir la réponse révisée ci-dessus.
Brett
4
var secondEmails = (from item in list2
                    select new { Email = item.Email }
                   ).ToList();

var matches = from item in list1
              where !secondEmails.Contains(item.Email)
              select new {Email = item.Email};
tvanfosson
la source
4

On pourrait également utiliser All()

var notInList = list1.Where(p => list2.All(p2 => p2.Email != p.Email));
Janis S.
la source
2

Bien que cela Exceptfasse partie de la réponse, ce n'est pas la réponse entière. Par défaut, Except(comme plusieurs des opérateurs LINQ) effectue une comparaison de référence sur les types de référence. Pour comparer par valeurs dans les objets, vous devrez

  • implémenter IEquatable<T>dans votre type, ou
  • remplacer Equalset GetHashCodedans votre type, ou
  • passer dans une instance d'un type implémentant IEqualityComparer<T>pour votre type
Ryan Lundy
la source
2
... si nous parlons de LINQ to Objects. S'il s'agissait de LINQ to SQL, la requête est traduite en instructions SQL qui s'exécutent sur la base de données, donc cela ne s'applique pas.
Lucas
1

Exemple utilisant List of int pour plus de simplicité.

List<int> list1 = new List<int>();
// fill data
List<int> list2 = new List<int>();
// fill data

var results = from i in list1
              where !list2.Contains(i)
              select i;

foreach (var result in results)
    Console.WriteLine(result.ToString());
Inisheer
la source
1

Pour tous ceux qui souhaitent également utiliser un INopérateur similaire à SQL en C #, téléchargez ce package:

Mshwf.NiceLinq

Il a Inet NotInméthodes:

var result = list1.In(x => x.Email, list2.Select(z => z.Email));

Même vous pouvez l'utiliser de cette façon

var result = list1.In(x => x.Email, "[email protected]", "[email protected]", "[email protected]");
mshwf
la source
0

Merci, Brett. Votre suggestion m'a également aidé. J'avais une liste d'objets et je voulais filtrer cela en utilisant une autre liste d'objets. Merci encore....

Si quelqu'un a besoin, veuillez consulter mon exemple de code:

'First, get all the items present in the local branch database
Dim _AllItems As List(Of LocalItem) = getAllItemsAtBranch(BranchId, RecordState.All)

'Then get the Item Mappings Present for the branch
Dim _adpt As New gItem_BranchesTableAdapter
Dim dt As New ds_CA_HO.gItem_BranchesDataTable
    _adpt.FillBranchMappings(dt, BranchId)

Dim _MappedItems As List(Of LocalItem) = (From _item As LocalItem In _AllItems Join _
    dr As ds_CA_HO.gItem_BranchesRow In dt _
    On _item.Id Equals dr.numItemID _
    Select _item).ToList

_AllItems = _AllItems.Except(_MappedItems.AsEnumerable).ToList

 Return _AllItems
mangeshkt
la source
0

Je n'ai pas testé cela avec LINQ to Entities :

NorthwindDataContext dc = new NorthwindDataContext();    
dc.Log = Console.Out;

var query =    
    from c in dc.Customers 
    where !dc.Orders.Any(o => o.CustomerID == c.CustomerID)   
    select c;

Alternativement:

NorthwindDataContext dc = new NorthwindDataContext();    
dc.Log = Console.Out;

var query =    
    from c in dc.Customers 
    where dc.Orders.All(o => o.CustomerID != c.CustomerID)   
    select c;

foreach (var c in query) 
    Console.WriteLine( c );
Tarik
la source
0

Impossible de faire une jointure externe, en sélectionnant uniquement les éléments de la première liste si le groupe est vide? Quelque chose comme:

Dim result = (From a In list1
              Group Join b In list2 
                  On a.Value Equals b.Value 
                  Into grp = Group
              Where Not grp.Any
              Select a)

Je ne sais pas si cela fonctionnerait d'une manière efficace avec le cadre Entity.

Marten Jacobs
la source
0

Vous pouvez également faire comme ceci:

var result = list1.Where(p => list2.All(x => x.Id != p.Id));
nzrytmn
la source