1. Introdução
Imagina um problema clássico: você tem duas coleções que estão logicamente ligadas. Por exemplo, uma lista de categorias de produtos e uma lista dos próprios produtos. Você precisa, pra cada categoria, pegar todos os produtos dessa categoria. Ou, por exemplo, tem uma lista de departamentos da empresa e uma lista de funcionários, e precisa mostrar todos os funcionários de cada departamento.
No SQL isso se chama "junção em grupo" (GROUP JOIN ou, mais precisamente, LEFT OUTER JOIN com agrupamento). No LINQ tem um operador especial pra isso – GroupJoin. Ele é tipo um meio termo entre a junção normal (Join), onde cada registro da esquerda tem exatamente um da direita, e o agrupamento por chave. O GroupJoin liga cada elemento de uma coleção com todos os elementos relacionados da outra coleção em forma de coleção.
Analogia
Se o Join normal é tipo juntar pares “pai e filho” pelo sobrenome, o GroupJoin é montar uma árvore: pra cada pai, uma lista de todos os filhos dele.
Esquematicamente
Categorias Produtos
+--------------+ +---------------------+
| Id | Nome | | Nome | CatId |
+----+---------+ +-----------+---------+
| 1 | Pão | ---> | Pãozinho | 1 |
| 2 | Bebidas | | Linguiça | 3 |
| 3 | Carne | | Pepsi | 2 |
| | | | Chá | 2 |
+----+---------+ +-----------+---------+
Depois do GroupJoin:
- Pão — [Pãozinho]
- Bebidas — [Pepsi, Chá]
- Carne — [Linguiça]
2. Assinatura do método e conceitos básicos
Método de extensão
public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer, // Coleção "externa" (tipo categorias)
IEnumerable<TInner> inner, // Coleção "interna" (tipo produtos)
Func<TOuter, TKey> outerKeySelector, // Como pegar a chave do elemento externo
Func<TInner, TKey> innerKeySelector, // Como pegar a chave do elemento interno
Func<TOuter, IEnumerable<TInner>, TResult> resultSelector // Fábrica pra criar o objeto/registro de resultado
)
- outer: coleção que vai ser percorrida e à qual os elementos vão ser ligados (tipo categorias).
- inner: coleção de onde vêm os elementos ligados (tipo produtos).
- outerKeySelector: lambda que retorna a chave do elemento "da esquerda".
- innerKeySelector: lambda que retorna a chave do elemento "da direita".
- resultSelector: função que define como vai ser o resultado pra cada par (esquerda+grupo da direita).
3. Exemplo prático: categorias e produtos
Suponha que temos esses modelos:
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Product
{
public string Name { get; set; }
public int CategoryId { get; set; }
}
Coleções pra exemplo:
var categories = new List<Category>
{
new Category { Id = 1, Name = "Pão" },
new Category { Id = 2, Name = "Bebidas" },
new Category { Id = 3, Name = "Carne" }
};
var products = new List<Product>
{
new Product { Name = "Pãozinho", CategoryId = 1 },
new Product { Name = "Pepsi", CategoryId = 2 },
new Product { Name = "Chá", CategoryId = 2 },
new Product { Name = "Linguiça", CategoryId = 3 }
};
Usando GroupJoin (Method Syntax)
var groupJoin = categories.GroupJoin(
products,
category => category.Id, // chave da categoria
product => product.CategoryId, // chave do produto
(category, prods) => new // monta o resultado na hora
{
CategoryName = category.Name,
Products = prods.Select(p => p.Name).ToList() // lista dos nomes dos produtos dessa categoria
}
);
Como percorrer o resultado:
foreach (var group in groupJoin)
{
Console.WriteLine($"Categoria: {group.CategoryName}");
foreach (var product in group.Products)
{
Console.WriteLine($" - {product}");
}
}
Saída:
Categoria: Pão
- Pãozinho
Categoria: Bebidas
- Pepsi
- Chá
Categoria: Carne
- Linguiça
4. GroupJoin: Query Syntax (sintaxe de consulta)
O LINQ suporta uma sintaxe parecida com SQL. Pra group join, usa a palavra-chave join ... into ..., e essa consulta funciona quase igual ao exemplo acima.
var groupJoin2 = from c in categories
join p in products on c.Id equals p.CategoryId into prodGroup
select new
{
CategoryName = c.Name,
Products = prodGroup.Select(p => p.Name).ToList()
};
Isso é bem parecido com uma consulta SQL usando LEFT OUTER JOIN ... GROUP BY.
Esquema visual: como funciona o GroupJoin
[Categoria] [Product] Agrupamento (GroupJoin)
Pão --------> Pãozinho => Pão: [Pãozinho]
Bebidas --------> Pepsi => Bebidas: [Pepsi, Chá]
Bebidas --------> Chá
Carne --------> Linguiça => Carne: [Linguiça]
Cada categoria ganha seu “bolsinho” (IEnumerable<Product>), onde caem todos os produtos dessa categoria.
5. Particularidades e pegadinhas
GroupJoin vs. Join normal
A diferença entre o Join normal e o GroupJoin tá na quantidade de resultados. O Join retorna um par pra cada combinação, já o GroupJoin retorna um elemento pra cada item da coleção externa, e dentro dele — uma coleção com todos os elementos que bateram.
Se na nossa tabela tiver uma categoria sem produtos, com o GroupJoin ela ainda vai aparecer, só que a lista de produtos dela vai estar vazia. Esse comportamento é igual ao LEFT OUTER JOIN no SQL (junção externa à esquerda).
Olha um exemplo com uma categoria sem produtos:
categories.Add(new Category { Id = 4, Name = "Queijos" });
var groupJoin3 = categories.GroupJoin(
products,
c => c.Id,
p => p.CategoryId,
(c, prods) => new
{
CategoryName = c.Name,
Products = prods.Select(p => p.Name).ToList()
});
foreach (var group in groupJoin3)
{
Console.WriteLine($"Categoria: {group.CategoryName}");
if (group.Products.Count == 0)
Console.WriteLine(" (Sem produtos)");
else
foreach (var product in group.Products)
Console.WriteLine($" - {product}");
}
Categoria: Pão
- Pãozinho
Categoria: Bebidas
- Pepsi
- Chá
Categoria: Carne
- Linguiça
Categoria: Queijos
(Sem produtos)
Esse cenário é bem comum em apps de negócio: precisa mostrar todas as categorias (ou grupos), mesmo que alguma delas não tenha itens.
Dá pra fazer com GroupBy? Não!
Muita gente se confunde tentando “imitar” o GroupJoin usando GroupBy duas vezes. Não faz isso — o GroupJoin foi feito justamente pra isso, e faz o “join à esquerda” de forma nativa.
6. Usando com dados reais
Além das aulas anteriores, bora adicionar no nosso app de exemplo uma função pra mostrar um relatório: “Pra cada categoria — lista dos produtos dela”. Isso é muito usado em lojas virtuais, CRMs, sistemas de controle ou de relatório.
Adiciona esse código no nosso app demo:
// Supondo que temos as classes Category e Product e as coleções já criadas
Console.WriteLine("RELATÓRIO DE CATEGORIAS E PRODUTOS:");
var categoryReport = categories.GroupJoin(
products,
cat => cat.Id,
prod => prod.CategoryId,
(cat, prods) => new
{
cat.Name,
ProductNames = prods.Select(p => p.Name).ToList()
});
foreach (var row in categoryReport)
{
Console.WriteLine($"Categoria: {row.Name}");
if (row.ProductNames.Count == 0)
Console.WriteLine(" (Sem produtos)");
else
foreach (var prodName in row.ProductNames)
Console.WriteLine($" - {prodName}");
}
7. Agrupamentos aninhados e agregações
Você pode combinar o GroupJoin com funções agregadas pra fazer relatórios mais complexos.
Exemplo: Contar quantos produtos tem em cada categoria
var reportWithCount = categories.GroupJoin(
products,
category => category.Id,
product => product.CategoryId,
(category, prods) => new
{
Category = category.Name,
Count = prods.Count() // Função agregada!
});
foreach (var rec in reportWithCount)
{
Console.WriteLine($"{rec.Category}: {rec.Count} produtos");
}
Suponha que as coleções categories e products tenham esses dados:
var categories = new[]
{
new { Id = 1, Name = "Frutas" },
new { Id = 2, Name = "Legumes" },
new { Id = 3, Name = "Laticínios" }
};
var products = new[]
{
new { Id = 1, Name = "Maçã", CategoryId = 1 },
new { Id = 2, Name = "Banana", CategoryId = 1 },
new { Id = 3, Name = "Cenoura", CategoryId = 2 }
};
Saída no console vai ser:
Categoria: Frutas — 2 produto(s)
Categoria: Legumes — 1 produto(s)
Categoria: Laticínios — 0 produto(s)
8. GroupJoin e erros comuns de quem tá começando
Um erro comum é achar que o resultado do GroupJoin vai ser uma tabela plana de pares, igual ao Join normal. "Plana" aqui quer dizer uma estrutura onde cada linha é um par: um elemento externo e um interno correspondente (tipo uma tabela SQL depois de um INNER JOIN).
Isso confunde principalmente quem já mexeu um pouco com banco de dados: esperam que o GroupJoin se comporte como um LEFT JOIN, mas retornando pares em linhas, não grupos. Só que o GroupJoin retorna um elemento da coleção externa e uma coleção dos internos relacionados — ou seja, uma estrutura aninhada.
Não esquece de "abrir" as coleções aninhadas quando precisar — tipo usando SelectMany, se quiser uma sequência normal de pares.
Outro erro comum é esquecer que, pra elementos sem correspondência, o subgrupo vai ser só uma lista vazia. Isso é o padrão, não é bug — mas é bom lembrar pra não se surpreender quando "nada acontece" no resultado.
Quando usar GroupJoin e quando não?
Use o GroupJoin:
- Quando você tem dois conjuntos de dados (tipo departamentos e funcionários, categorias e produtos) e quer mostrar de forma hierárquica: pra cada “pai” todos os “filhos”.
- Pra montar relatórios complexos, onde é importante mostrar todos os itens principais mesmo que não tenham “filhos”.
- Quando precisa de algo parecido com o LEFT OUTER JOIN do SQL, agrupando por chave.
Não use o GroupJoin quando só precisa cruzar coleções ou pegar exatamente um par por correspondência — pra isso tem o Join normal.
GO TO FULL VERSION