O Entity Framework Core 10 introduziu uma das melhorias mais aguardadas pela comunidade .NET: os métodos LeftJoin() e RightJoin() como parte integrante da API LINQ oficial do ORM. Pela primeira vez, é possível expressar junções externas de forma direta, legível e com suporte completo a tradução para SQL ANSI-compliant em todos os provedores relacionais suportados.
Esse avanço elimina definitivamente a necessidade de construções verbosas e pouco intuitivas baseadas em GroupJoin + DefaultIfEmpty + SelectMany, que eram a única forma nativa de implementar left e right outer joins no EF Core até a versão 9.0. A mudança não é apenas sintática: ela melhora a manutenibilidade, reduz erros de lógica, facilita a revisão de código e garante geração de SQL mais previsível e otimizada.
Como era antes do .NET 10
Antes do EF Core 10, a única maneira de expressar um left join em LINQ era através da combinação de GroupJoin com DefaultIfEmpty e SelectMany. Essa construção, embora funcional, apresentava múltiplos pontos de fragilidade:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// ANTES: Left Join (clientes com ou sem pedidos) – versão completa com SelectMany var clientesComPedidos = contexto.Clientes .GroupJoin( contexto.Pedidos, cliente => cliente.Id, pedido => pedido.ClienteId, (cliente, pedidos) => new { cliente, pedidos }) .SelectMany( x => x.pedidos.DefaultIfEmpty(), (cliente, pedido) => new { cliente.cliente.Nome, PedidoId = pedido != null ? pedido.Id : (int?)null, DataPedido = pedido != null ? pedido.Data : (DateTime?)null }); |
Problemas evidentes:
- Legibilidade comprometida: a intenção do join externo não é clara à primeira leitura.
- Erro comum: esquecer o DefaultIfEmpty() gera um inner join silencioso.
- Complexidade desnecessária: o uso obrigatório de SelectMany para “desagrupar” o resultado do GroupJoin.
- SQL gerado imprevisível: em alguns provedores, resultava em subqueries, CROSS APPLY ou APPLY implícito.
- Dificuldade de manutenção: novos desenvolvedores precisavam aprender um padrão não intuitivo e propenso a erros.
Para right join, a situação era ainda pior: não havia forma nativa. Era necessário inverter a ordem das tabelas e simular com left join, ou recorrer a SQL bruto.
LeftJoin() e RightJoin() nativos
Com o EF Core 10, a sintaxe torna-se direta e alinhada com a semântica SQL:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// DEPOIS: Left Join (clientes com ou sem pedidos) var resultado = await contexto.Clientes .LeftJoin( contexto.Pedidos, cliente => cliente.Id, pedido => pedido.ClienteId, (cliente, pedido) => new { cliente.Nome, PedidoId = pedido != null ? pedido.Id : (int?)null, DataPedido = pedido != null ? pedido.Data : (DateTime?)null }) .ToListAsync(); |
O método RightJoin() segue a mesma assinatura, apenas invertendo a direção da junção.
|
1 2 3 |
SELECT [c].[Nome], [p].[Id] AS [PedidoId], [p].[Data] AS [DataPedido] FROM [Clientes] AS [c] LEFT OUTER JOIN [Pedidos] AS [p] ON [c].[Id] = [p].[ClienteId] |
- Sem subqueries desnecessárias
- Sem CROSS APPLY ou APPLY implícito
- Tradução 1:1 com SQL Server, PostgreSQL e MySQL
- Otimização de plano idêntica à de um LEFT JOIN escrito manualmente
Exemplo completo: clientes sem pedidos
|
1 2 3 4 5 6 7 8 9 |
var clientesSemPedidos = await contexto.Clientes .LeftJoin( contexto.Pedidos, c => c.Id, p => p.ClienteId, (c, p) => new { Cliente = c, Pedido = p }) .Where(x => x.Pedido == null) .Select(x => x.Cliente) .ToListAsync(); |
Equivalente a:
|
1 2 3 4 |
SELECT [c].* FROM [Clientes] AS [c] LEFT OUTER JOIN [Pedidos] AS [p] ON [c].[Id] = [p].[ClienteId] WHERE [p].[Id] IS NULL |
O EF Core 10 mantém o comportamento esperado de eager loading mesmo em junções externas.
Conclusão
A introdução de LeftJoin() e RightJoin() no EF Core 10 não é apenas uma melhoria de conveniência: é uma correção de uma dívida técnica histórica que afetava diretamente a produtividade, a qualidade do código e a confiabilidade das consultas em aplicações .NET corporativas.
A partir de hoje, não há mais justificativa técnica para manter construções baseadas em GroupJoin + DefaultIfEmpty + SelectMany em novos projetos e há forte recomendação para refatorar gradualmente os existentes.
FAQ: Perguntas Frequentes
1. LeftJoin() substitui completamente GroupJoin + DefaultIfEmpty + SelectMany()?
Sim. O padrão antigo torna-se obsoleto a partir do .NET 10, embora mantido por compatibilidade.
2. É possível fazer Full Outer Join?
Sim, combinando LeftJoin com RightJoin ou usando o método FullJoin() (em preview no EF Core 10.1)
3. Funciona com AsSplitQuery() ou AsSingleQuery()?
Sim. O comportamento é idêntico ao de inner joins tradicionais.
4. O SQL gerado é otimizado em todos os provedores?
Sim. Tradução direta para LEFT/RIGHT OUTER JOIN em todos os bancos relacionais suportados.
5. Posso usar com projeção anônima ou DTOs?
- Sim. O resultado do join pode ser projetado diretamente em qualquer tipo, incluindo records e DTOs.