Como você faria uma consulta "in-in" com o LINQ?

307

Eu tenho duas coleções que possuem propriedades Emailem ambas as coleções. Preciso obter uma lista dos itens na primeira lista onde Emailnão existe na segunda lista. Com o SQL, eu usaria "not in", mas não sei o equivalente no LINQ. Como isso é feito?

Até agora eu tenho uma junção, como ...

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

Mas não posso participar, pois preciso da diferença e a associação falharia. Eu preciso de alguma maneira de usar Contém ou Existe, acredito. Eu apenas não encontrei um exemplo para fazer isso ainda.

Brennan
fonte
3
Por favor, note que a resposta de Echostorm produz código que é muito mais clara de ler do que de Robert
Nathan Koop

Respostas:

302

Eu não sei se isso irá ajudá-lo, mas ..

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

da cláusula NOT IN no LINQ to SQL por Marco Russo

Robert Rouse
fonte
Mas eu uso linq para entidades, então recebo "apenas tipos primitivos podem ser usados ​​com erro". Existe algum trabalho em torno de ...? além de iterar manualmente e encontrar a lista.
Novice
13
Isso funciona bem para mim com o LINQ to Entities. O SQL se torna uma consulta WHERE NOT EXISTS (subconsulta). Talvez tenha havido uma atualização que resolva isso?
22412 scottheckel
2
Eu acho que as versões mais recentes do EF fazer .Contains de apoio, além de esta pergunta não faz tag EF (versão) ou LinqToSQL .. então pode haver uma necessidade de espaço de perguntas e respostas aqui ..
Brett Caswell
4
@Robert Rouse - O link para o Not in cluse no linq to sql não funciona mais. Apenas um FYI.
21416 JonH
O link fornecido leva a um site sinalizado como contendo malware.
Mikeigs
334

Você deseja o operador Exceto.

var answer = list1.Except(list2);

Melhor explicação aqui: https://docs.microsoft.com/archive/blogs/charlie/linq-farm-more-on-set-operators

NOTA: Essa técnica funciona melhor apenas para tipos primitivos, pois você precisa implementar um IEqualityComparer para usar o Exceptmétodo com tipos complexos.

Echostorm
fonte
7
Usando Exceto: Se você trabalha com listas de tipos complexos, então você tem que implementar um IEqualityComparer <MyComlplexType>, que torna-se não que Nice
Sakito
4
Você não precisa implementar IEqualityComparer <T> se quiser comparar a igualdade de referência ou se substituiu T.Equals () e T.GetHashCode (). Se você não implementar IEqualityComparer <T>, o EqualityComparer <T> .Default será usado.
Piedar
2
@Echostorm (e outros que estão lendo), se você fizer um objeto Select to Anonymous, o HashCode será determinado pelos valores da propriedade; list1.Select(item => new { Property1 = item.Property1, Property2 = item.Property2 }).Except(list2.Select( item => new { Property1 = item.Property1, Property2 = item.Property2 }));isso é particularmente útil quando você determina a igualdade, avaliando apenas um conjunto de valores do tipo complexo.
Brett Caswell
3
Na verdade, alguém apontou abaixo, e penso corretamente, que não haveria necessidade de implementar IEquatityComparor<T,T>ou substituir métodos de comparação de objetos em um LinqToSqlcenário; pois, a consulta será representada como / compilada para / expressa como SQL; portanto, os valores serão verificados, não a referência do objeto.
Brett Caswell
2
Usando o exceptque eu era capaz de acelerar uma consulta LINQ de 8-10 segundos a um meio segundo
Michael Kniskern
61

Para pessoas que começam com um grupo de objetos na memória e estão consultando um banco de dados, descobri que esse é o melhor caminho a seguir:

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

Isso produz uma boa WHERE ... IN (...)cláusula no SQL.

StriplingWarrior
fonte
1
na verdade, você pode fazer isso no 3.5
George Silva
59

itens na primeira lista em que o email não existe na segunda lista.

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

Você pode usar uma combinação de Onde e Qualquer para encontrar não em:

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

Você pode pegar as duas coleções em duas listas diferentes, como list1 e list2.

Então é só escrever

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

Isso vai funcionar.

Chintan Udeshi
fonte
3
Bom, mas tem o efeito colateral de remover elementos da lista.
Tarik
7

No caso de alguém estar usando o ADO.NET Entity Framework , a solução da EchoStorm também funciona perfeitamente. Mas levei alguns minutos para envolver minha cabeça. Supondo que você tenha um contexto de banco de dados, dc e deseje encontrar linhas na tabela x não vinculadas na tabela y, a resposta completa da resposta será semelhante a:

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

Em resposta ao comentário de Andy, sim, é possível ter dois de em uma consulta LINQ. Aqui está um exemplo de trabalho completo, usando listas. Cada classe, Foo e Bar, tem um ID. Foo tem uma referência de "chave estrangeira" a Bar via Foo.BarId. O programa seleciona todos os Foo não vinculados a uma barra correspondente.

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
fonte
fazer dois froms wor em LINQ? isso seria útil.
Andy
Andy: Sim, veja a resposta revisada acima.
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
fonte
4

Pode-se também usar All()

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

Embora faça Exceptparte da resposta, não é a resposta completa. Por padrão, Except(como vários operadores LINQ), faz uma comparação de referência nos tipos de referência. Para comparar por valores nos objetos, você precisará

  • implementar IEquatable<T>em seu tipo ou
  • substituir Equalse GetHashCodeno seu tipo ou
  • passar em uma instância de um tipo implementado IEqualityComparer<T>para o seu tipo
Ryan Lundy
fonte
2
... se estamos falando de LINQ to Objects. Se fosse LINQ to SQL, a consulta é convertida em instruções SQL executadas no banco de dados, portanto, isso não se aplica.
Lucas
1

Exemplo usando Lista de int por simplicidade.

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
fonte
1

Para quem também deseja usar um INoperador semelhante ao SQL em C #, baixe este pacote:

Mshwf.NiceLinq

Possui Ine NotInmétodos:

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

Até você pode usá-lo dessa maneira

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

Obrigado, Brett. Sua sugestão também me ajudou. Eu tinha uma lista de objetos e queria filtrar isso usando outra lista de objetos. Obrigado novamente....

Se alguém precisar, dê uma olhada no meu exemplo de código:

'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
fonte
0

Não testei isso com o 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;

Alternativamente:

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
fonte
0

Não foi possível fazer uma associação externa, apenas selecionando os itens da primeira lista se o grupo estiver vazio? Algo como:

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)

Não tenho certeza se isso funcionaria de alguma forma eficiente com a estrutura da Entidade.

Marten Jacobs
fonte
0

Como alternativa, você pode fazer assim:

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