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:
// 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:
// 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.
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
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:
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.