Comportamento
Suponha que você tenha duas listas:
Id Value
1 A
2 B
3 C
Id ChildValue
1 a1
1 a2
1 a3
2 b1
2 b2
Quando você Joinas duas listas no Idcampo, o resultado será:
Value ChildValue
A a1
A a2
A a3
B b1
B b2
Quando você GroupJoinas duas listas no Idcampo, o resultado será:
Value ChildValues
A [a1, a2, a3]
B [b1, b2]
C []
Portanto, Joinproduz um resultado simples (tabular) dos valores pai e filho.
GroupJoinproduz uma lista de entradas na primeira lista, cada uma com um grupo de entradas unidas na segunda lista.
É por isso que Joiné equivalente INNER JOINno SQL: não há entradas para C. Enquanto GroupJoiné o equivalente a OUTER JOIN: Cestá no conjunto de resultados, mas com uma lista vazia de entradas relacionadas (em um conjunto de resultados SQL, haveria uma linha C - null).
Sintaxe
Então deixe as duas listas serem IEnumerable<Parent>e IEnumerable<Child>respectivamente. (No caso de Linq to Entities:) IQueryable<T>.
Join sintaxe seria
from p in Parent
join c in Child on p.Id equals c.Id
select new { p.Value, c.ChildValue }
retornando um IEnumerable<X>onde X é um tipo anônimo com duas propriedades Valuee ChildValue. Essa sintaxe de consulta usa o Joinmétodo sob o capô.
GroupJoin sintaxe seria
from p in Parent
join c in Child on p.Id equals c.Id into g
select new { Parent = p, Children = g }
retornando um IEnumerable<Y>onde Y é um tipo anônimo que consiste em uma propriedade do tipo Parente uma propriedade do tipo IEnumerable<Child>. Essa sintaxe de consulta usa o GroupJoinmétodo sob o capô.
Poderíamos fazer select gna última consulta, que selecionaria uma IEnumerable<IEnumerable<Child>>, digamos uma lista de listas. Em muitos casos, a seleção com o pai incluído é mais útil.
Alguns casos de uso
1. Produzindo uma junção externa plana.
Como disse, a declaração ...
from p in Parent
join c in Child on p.Id equals c.Id into g
select new { Parent = p, Children = g }
... produz uma lista de pais com grupos de crianças. Isso pode ser transformado em uma lista simples de pares pai-filho por duas pequenas adições:
from p in parents
join c in children on p.Id equals c.Id into g // <= into
from c in g.DefaultIfEmpty() // <= flattens the groups
select new { Parent = p.Value, Child = c?.ChildValue }
O resultado é semelhante ao
Value Child
A a1
A a2
A a3
B b1
B b2
C (null)
Observe que a variável range c é reutilizada na instrução acima. Fazendo isso, qualquer joininstrução pode simplesmente ser convertida em uma outer joinadicionando o equivalente into g from c in g.DefaultIfEmpty()a uma joininstrução existente .
É aqui que a sintaxe da consulta (ou abrangente) brilha. A sintaxe do método (ou fluente) mostra o que realmente acontece, mas é difícil escrever:
parents.GroupJoin(children, p => p.Id, c => c.Id, (p, c) => new { p, c })
.SelectMany(x => x.c.DefaultIfEmpty(), (x,c) => new { x.p.Value, c?.ChildValue } )
Portanto, um apartamento outer joinno LINQ é um GroupJoin, achatado por SelectMany.
2. Preservando a ordem
Suponha que a lista de pais seja um pouco mais longa. Algumas UI produzem uma lista de pais selecionados como Idvalores em uma ordem fixa. Vamos usar:
var ids = new[] { 3,7,2,4 };
Agora os pais selecionados devem ser filtrados da lista de pais nesta ordem exata.
Se nós fizermos ...
var result = parents.Where(p => ids.Contains(p.Id));
... a ordem de parentsdeterminará o resultado. Se os pais são ordenados por Id, o resultado será os pais 2, 3, 4, 7. Não é bom. No entanto, também podemos usar joinpara filtrar a lista. E, usando idscomo primeira lista, o pedido será preservado:
from id in ids
join p in parents on id equals p.Id
select p
O resultado são os pais 3, 7, 2, 4.