Mudanças no EF Core 5 que todos aguardamos

O EF Core 5 marca uma virada importante no Entity Framework Core, porque traz de volta recursos que a comunidade pedia havia anos. Neste artigo, você entende o que mudou, por que essas mudanças importam e como elas afetam a forma de modelar e consultar dados no .NET.

Quando o plano da versão foi publicado, a pergunta era simples: será que as preces da comunidade seriam ouvidas? Hoje, com a versão lançada, dá para responder com fatos e código.

O que é um ORM

Antes de falar das novidades, vale pontuar o que de fato é um ORM. ORM é a sigla para Object/Relational Mapping, ou seja, mapeamento objeto/relacional.

A função dele é mapear o resultado que recebemos de um banco de dados relacional para um objeto. Note que eu disse objeto e não entidade, porque o destino pode ser um DTO, uma entidade anêmica ou qualquer outra classe.

Em outras palavras, o ORM traduz linhas e colunas em instâncias de classes e faz o caminho inverso na hora de salvar. Com isso, você escreve LINQ em vez de SQL na maior parte do tempo.

Caso queira saber mais sobre modelagem de domínios ricos, acesse meu curso gratuito sobre o assunto clicando aqui.

A trajetória do Entity Framework Core

Na versão 1.x do .NET Core, muita coisa foi prometida. Contudo, como as mudanças eram muitas, minha expectativa não estava lá em cima, pois eu sabia que o .NET Core ainda não estava maduro e o EF Core muito menos.

O item que mais me deixou desapontado foi o mapeamento NxN. Antes, no Entity Framework 6, ele funcionava com navigation properties nas duas pontas, mas no EF Core passou a exigir uma classe exclusiva para a tabela de junção.

Em termos de modelagem, isso é muito ruim. Sabemos que não conseguimos ter o cenário perfeito de orientação a objetos usando um ORM, mas afetar dessa forma a modelagem chega a doer na alma.

Na versão 2.x vieram várias melhorias de performance e de consultas. Na 3.x, o time reescreveu a tradução de queries, mas nada do NxN mudar.

A proposta do EF Core: data driven ou domain driven

Foi então que decidi parar de brigar e assumir que a proposta do EF Core é mais data driven do que domain driven. Olhando o cenário NxN, vemos claramente uma alusão aos relacionamentos de bancos de dados, e não à orientação a objetos.

Encarar dessa forma facilitou encaixar o uso do EF Core no dia a dia. Em cenários data driven ele brilha, enquanto em cenários focados em domínios ricos ou usamos outra ferramenta ou desmanchamos nossas entidades.

Por exemplo, muita gente combina o EF Core para escrita com o Dapper para leitura. Esse equilíbrio aparece no artigo Dapper e Entity Framework Core, que compara as duas abordagens.

Se quiser saber mais sobre a criação de APIs data driven com ASP.NET Core 3 e EF Core 3, acesse meu curso clicando aqui.

A comunidade e o planejamento do EF Core 5

Outro ponto que me incomodava bastante é que muitas das features abaixo estavam como sugestão no GitHub do EF Core havia tempos. Mesmo assim, elas vinham ficando de fora das versões.

Não estou julgando, pois não sei como as coisas acontecem internamente. No entanto, a implementação dessas features no EF Core 5 mostra que o time passou a se posicionar de forma diferente, ouvindo mais a comunidade.

Os números do próprio plano oficial do EF Core 5.0 confirmam isso. O many-to-many era a feature mais votada do backlog, com cerca de 506 votos, o filtered include vinha em segundo, com cerca de 376, e o TPT em terceiro, com cerca de 289.

Ou seja, as três maiores demandas da comunidade entraram no EF Core 5. Além disso, o processo de planejamento da Microsoft passou a deixar claro que os votos nas issues do GitHub pesam nas decisões.

Mudanças no EF Core 5

A primeira mudança está na própria versão. Não existe EF Core 4.x, porque o time pulou direto para o EF Core 5 para acompanhar o .NET 5.

Além disso, o EF Core 5 roda em qualquer plataforma compatível com o .NET Standard 2.1, incluindo o .NET 5. Por outro lado, ele não roda no .NET Framework, o que encerra de vez essa compatibilidade.

Embora a mudança seja grande, o time prometeu breaking changes bem menos severas que as do EF Core 3.0. A meta declarada era permitir que a grande maioria das aplicações atualizasse sem quebrar.

Sem mais delongas, vamos às seis mudanças mais aguardadas no Entity Framework Core 5.

Many-to-many no EF Core 5

Eu sei que deveria deixar esta feature para o fim, mas não estou aguentando de felicidade. Finalmente o EF Core 5 muda o mapeamento NxN, com a volta das navigation properties nas duas pontas.

O modelo com classe de junção explícita continua suportado. Porém, agora temos a alternativa de fazer como antigamente, apenas com duas coleções, uma em cada entidade.

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }

    // Coleção de tags, sem classe de junção
    public ICollection<Tag> Tags { get; set; }
}

public class Tag
{
    public int Id { get; set; }
    public string Name { get; set; }

    // Coleção inversa, fecha o relacionamento NxN
    public ICollection<Post> Posts { get; set; }
}

Nesse caso, o EF Core 5 reconhece o relacionamento por convenção e cria a tabela PostTag apenas no nível do banco de dados. Dessa forma, você consulta e atualiza os dados sem referenciar a tabela de junção no código.

Por trás disso estão as chamadas skip navigations, que permitem navegar de Post para Tag pulando a entidade intermediária. Ainda assim, a tabela de junção pode ser customizada e consultada quando necessário.

Table per type (TPT)

Outra mudança legal é a possibilidade de mapear cada tipo de uma hierarquia de herança em uma tabela própria. Por padrão, o EF Core usa o table per hierarchy (TPH), que coloca a hierarquia inteira em uma única tabela.

Com o TPT, a classe base ganha uma tabela, e cada classe derivada ganha outra, ligada à base pela chave primária.

public abstract class Payment
{
    public int Id { get; set; }
    public decimal Amount { get; set; }
}

public class CreditCardPayment : Payment
{
    public string CardHolder { get; set; }
}

public class BankSlipPayment : Payment
{
    public string BarCode { get; set; }
}

public class StoreContext : DbContext
{
    public DbSet<Payment> Payments { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Cada tipo da hierarquia vira uma tabela
        modelBuilder.Entity<CreditCardPayment>().ToTable("CreditCardPayments");
        modelBuilder.Entity<BankSlipPayment>().ToTable("BankSlipPayments");
    }
}

Assim, a herança fica mais natural quando você precisa separar os tipos em tabelas à parte. Contudo, o TPT exige joins nas consultas, então vale medir a performance antes de adotá-lo em hierarquias grandes.

Filtered include e split queries no EF Core 5

Fazer consultas quando há Include envolvido sempre foi um desafio. Muitas vezes partimos para LINQ manual, Dapper ou até ADO.NET, mas o EF Core 5 melhorou bastante esse cenário.

Na época do plano, falava-se de consultas dentro do próprio Include. O recurso chegou com o nome filtered include, já que o aninhamento de includes com ThenInclude existia desde as versões anteriores.

// Traz os blogs e apenas os posts publicados, ordenados e limitados
var blogs = await context.Blogs
    .Include(blog => blog.Posts
        .Where(post => post.IsPublished)
        .OrderByDescending(post => post.PublishedAt)
        .Take(5))
    .ToListAsync();

Dentro do Include, o EF Core 5 aceita Where, OrderBy, OrderByDescending, ThenBy, ThenByDescending, Skip e Take. Com isso, você evita trazer coleções inteiras para filtrar em memória.

Outra novidade relacionada são as split queries. No EF Core 3.0, toda consulta LINQ virava um único SQL, o que gerava explosão cartesiana quando havia vários Include de coleções.

// Divide a consulta em vários comandos SQL, um por coleção
var orders = await context.Orders
    .Include(order => order.Items)
    .Include(order => order.Payments)
    .AsSplitQuery()
    .ToListAsync();

O operador AsSplitQuery pode melhorar muito a performance. Por outro lado, os dados podem mudar entre um comando e outro, então avalie o nível de isolamento da transação.

Migrations fora do startup

Para os fãs de DevOps, o plano trazia uma promessa: desacoplar as migrations da aplicação. Hoje, muita gente aplica a migração no startup do app, o que traz vários problemas.

A própria documentação lista os riscos. Vários servidores podem tentar migrar o banco ao mesmo tempo, e a aplicação pode acessar um estado inconsistente.

Além disso, o usuário da aplicação acaba com permissão para alterar o schema.

Também é difícil voltar para um estado limpo quando algo dá errado. Portanto, o caminho recomendado é aplicar as migrations no processo de deployment, como deve ser de fato.

No EF Core 5, essa frente foi entregue em partes. Os migration bundles foram adiados para depois da versão, mas chegaram melhorias pontuais, como o comando dotnet ef migrations list e o parâmetro --connection.

# Lista as migrations e mostra quais ainda não foram aplicadas
dotnet ef migrations list

# Aplica as migrations informando a connection string na linha de comando
dotnet ef database update --connection "Server=localhost;Database=Store;Trusted_Connection=True"

# Gera um script SQL idempotente para rodar no pipeline
dotnet ef migrations script --idempotent --output migrate.sql

Os scripts de migração também passaram a incluir instruções de transação quando apropriado. Se quiser ver migrations na prática, o artigo migrations com EF Core e PostgreSQL mostra o fluxo completo.

Experiência em outras plataformas

Outro tópico do plano era melhorar o uso do EF com Xamarin, Blazor, WinForms e WPF. Na época, deixei esse ponto em aberto, pois não tinha conhecimento para falar sobre ele.

No fim, o time publicou guias e exemplos para Blazor, Xamarin, WinForms e WPF. Já o trabalho de AOT e linker para Xamarin ficou para o EF Core 6.

Um recurso que ajudou bastante o Blazor foi o AddDbContextFactory. Ele registra uma fábrica no container de injeção de dependência, e você cria contextos de vida curta quando precisar.

// Registra a fábrica de contextos no container de DI
services.AddDbContextFactory<StoreContext>(options =>
    options.UseSqlServer(connectionString));

public class ProductService
{
    private readonly IDbContextFactory<StoreContext> _contextFactory;

    public ProductService(IDbContextFactory<StoreContext> contextFactory)
        => _contextFactory = contextFactory;

    public async Task<int> CountProductsAsync()
    {
        // Cada operação usa um contexto novo e descartável
        using var context = _contextFactory.CreateDbContext();
        return await context.Products.CountAsync();
    }
}

Performance e queries no EF Core 5

Além dos tópicos que mencionei, o EF Core 5 trouxe melhorias nas queries e na performance, assim como o time vinha fazendo nas versões anteriores. Como eu esperava, não houve uma revolução, e sim vários pontos específicos.

Por exemplo, a tradução de null semantics ficou mais enxuta e os blocos CASE no SQL ficaram mais concisos. O GroupBy também ganhou suporte a agregações condicionais.

As consultas sem rastreamento passaram a fazer identity resolution, evitando instâncias duplicadas do mesmo registro. Além disso, o tamanho padrão de lote no SQL Server mudou para 42 comandos, com base em análises de performance.

Na camada de diagnóstico, o EF Core 5 expõe event counters. Assim, você acompanha consultas por segundo, contextos ativos e taxa de acerto do cache de queries com a ferramenta dotnet-counters.

Outras novidades que chegaram com o EF Core 5

Além das seis mudanças acima, a versão final trouxe recursos que não estavam no centro do plano. Vários deles mudam o dia a dia de quem depura consultas.

O primeiro é o LogTo, uma forma simples de configurar logs. Em seguida vem o ToQueryString, que devolve o SQL que uma consulta LINQ vai executar.

public class StoreContext : DbContext
{
    public DbSet<Product> Products { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer("Server=localhost;Database=Store;Trusted_Connection=True")
            // Escreve no console todos os logs, incluindo o SQL gerado
            .LogTo(Console.WriteLine);
}

// Mostra o SQL sem executar a consulta
var sql = context.Products
    .Where(product => product.Price > 100)
    .ToQueryString();

Console.WriteLine(sql);

Outra novidade são os dependentes obrigatórios em relacionamentos um para um. No EF Core 3.1, todo owned type era opcional, e suas colunas eram criadas como anuláveis no banco.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Customer>(builder =>
    {
        builder.OwnsOne(customer => customer.Address, address =>
        {
            address.Property(a => a.Street).IsRequired();
            address.Property(a => a.ZipCode).IsRequired();
        });

        // O endereço passa a ser obrigatório no modelo
        builder.Navigation(customer => customer.Address).IsRequired();
    });
}

Também chegaram as collations por coluna, os atributos [Index] e [Keyless], a configuração de precisão e escala pela Fluent API e o ChangeTracker.Clear. No SQLite, o EF Core 5 passou a reconstruir tabelas automaticamente para suportar operações de migração antes impossíveis, como remover colunas.

Por fim, o mapeamento ficou mais flexível. Agora uma entidade pode apontar para uma view na leitura e para uma tabela na escrita, ou ainda para uma função com valor de tabela.

Como atualizar um projeto para o EF Core 5

Se você tem um projeto em EF Core 3.1, o primeiro passo é conferir o target framework. Como o EF Core 5 exige .NET Standard 2.1, o projeto precisa estar no .NET Core 3.x ou no .NET 5.

Em seguida, atualize os pacotes do provedor e das ferramentas para a mesma versão.

# Atualiza o provedor do SQL Server
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 5.0.0

# Atualiza o pacote de design usado pelas migrations
dotnet add package Microsoft.EntityFrameworkCore.Design --version 5.0.0

# Atualiza a ferramenta de linha de comando
dotnet tool update --global dotnet-ef --version 5.0.0

Depois, leia a lista oficial de breaking changes e rode a suíte de testes completa. Uma mudança comum, por exemplo, é o scaffolding singularizar nomes de tabelas, gerando Person a partir de People.

Vale lembrar que o suporte ao EF Core 5 terminou em 10 de maio de 2022, junto com o do .NET 5. Portanto, este conteúdo serve para entender a evolução do framework e para manter sistemas legados, enquanto projetos novos devem usar uma versão suportada.

Se você acompanhou a fase de prévia, o artigo sobre o Entity Framework Core 5 Preview 1 mostra como essas features começaram a aparecer.

Conclusão

O EF Core 5 respondeu às maiores demandas da comunidade. O many-to-many sem classe de junção, o TPT e o filtered include eram, nessa ordem, as três features mais votadas do backlog.

Além disso, as split queries corrigiram a principal regressão de performance do EF Core 3.0, e o LogTo com o ToQueryString simplificou a depuração. Já as migrations fora do startup avançaram em partes, com os bundles chegando só na versão seguinte.

Na minha visão, o EF Core 5 continua sendo um ORM mais data driven do que domain driven. Ainda assim, a volta das navigation properties no NxN aproxima bastante o modelo da orientação a objetos.

Em resumo, o time mostrou que passou a ouvir a comunidade. E isso, para quem usa o framework no dia a dia, vale tanto quanto qualquer feature nova.

FAQ

Por que não existe EF Core 4?

O time pulou a versão 4 para alinhar a numeração ao .NET 5. Dessa forma, EF Core 5 e .NET 5 foram lançados juntos.

O EF Core 5 roda no .NET Framework?

Não. Ele exige uma plataforma compatível com .NET Standard 2.1, como o .NET Core 3.x ou o .NET 5.

Preciso apagar minha classe de junção para usar o many-to-many?

Não. O modelo com classe de junção explícita continua suportado, e a nova abordagem com duas coleções é apenas uma alternativa.

Qual a diferença entre TPH e TPT?

No TPH, toda a hierarquia fica em uma única tabela com uma coluna discriminadora. Já no TPT, cada tipo ganha sua própria tabela, ligada à tabela base pela chave primária.

Quando usar AsSplitQuery?

Use quando a consulta inclui várias coleções e o SQL único gera muitas linhas duplicadas. Lembre que os dados podem mudar entre os comandos.

O EF Core 5 ainda tem suporte?

Não. O suporte terminou em 10 de maio de 2022, então projetos novos devem usar uma versão suportada.

Glossário

Termo Significado
ORM Object/Relational Mapping, ferramenta que mapeia dados relacionais para objetos.
Many-to-many (NxN) Relacionamento em que vários registros de um lado se ligam a vários do outro.
Skip navigation Navigation property que pula a entidade de junção em um relacionamento NxN.
TPH Table per hierarchy, estratégia que guarda toda a hierarquia de herança em uma tabela.
TPT Table per type, estratégia que cria uma tabela para cada tipo da hierarquia.
Filtered include Include que aceita filtros, ordenação e paginação na coleção carregada.
Split query Consulta LINQ dividida em vários comandos SQL.
Migration Conjunto de alterações versionadas no schema do banco de dados.
Owned type Tipo que pertence a uma entidade e não tem identidade própria.
Event counter Métrica de diagnóstico do .NET lida com ferramentas como o dotnet-counters.

Fontes