Introdução

Se você desenvolve com .NET, em algum momento precisou decidir como sua aplicação vai se comunicar com o banco de dados. Escrever SQL puro? Usar um micro ORM como o Dapper? Ou adotar um ORM completo como o Entity Framework Core? Essa decisão vai muito além de performance — ela impacta diretamente a manutenibilidade, a testabilidade e, principalmente, o acoplamento da sua aplicação com o banco de dados.

Neste artigo, vamos explorar o EF Core 8+ usando exclusivamente Fluent API para mapeamento de entidades — nunca Data Annotations. Vamos entender o que é um ORM, comparar os mais conhecidos do mercado, demonstrar por que o EF Core oferece uma vantagem tática e estratégica em relação a micro ORMs como o Dapper, e implementar todos os tipos de relacionamento possíveis com exemplos completos em C# 12. Se você já trabalha com EF Core e quer aprofundar no mapeamento avançado, ou se está avaliando qual estratégia de acesso a dados adotar no seu projeto, este guia é para você. Para entender como o EF Core se comporta em cenários de alta carga, veja também Gargalo em Banco de Dados com C# e EF Core.

📦 Código-fonte: A implementação completa deste artigo está no repositório blog-zocateli-sample no GitHub. Clone, explore e adapte ao seu contexto.


O Que É um ORM e Quais os Mais Conhecidos

ORM (Object-Relational Mapping) é uma técnica que mapeia objetos da sua linguagem de programação para tabelas de um banco de dados relacional. Em vez de escrever SQL manualmente, você manipula objetos e o ORM se encarrega de traduzir essas operações em comandos SQL — INSERT, UPDATE, DELETE, SELECT — de forma transparente.

O conceito surgiu para resolver o chamado impedance mismatch: a diferença fundamental entre o modelo orientado a objetos (com herança, polimorfismo, encapsulamento) e o modelo relacional (com tabelas, colunas, chaves estrangeiras). Um ORM faz essa ponte automaticamente.

ORMs Completos vs Micro ORMs

Os ORMs se dividem em duas categorias com filosofias bem diferentes:

TipoCaracterísticasExemplos
ORM CompletoChange tracking, migrations, LINQ, lazy loading, mapeamento automático de relacionamentosEF Core, Hibernate, NHibernate, SQLAlchemy, Django ORM
Micro ORMMapeamento simples de query → objeto, sem tracking, sem migrations, SQL manualDapper, PetaPoco, Massive

Principais ORMs do Mercado

ORMLinguagem/PlataformaObservação
Entity Framework CoreC# / .NETORM oficial da Microsoft. Open-source, cross-platform, suporte a múltiplos bancos
HibernateJavaO ORM mais maduro do ecossistema Java. Inspiração para o NHibernate
NHibernateC# / .NETPort do Hibernate para .NET. Robusto, mas com API mais verbosa que o EF Core
DapperC# / .NETMicro ORM criado pelo Stack Overflow. Rápido, leve, SQL puro
SQLAlchemyPythonORM mais popular do Python. Suporte a Core (baixo nível) e ORM (alto nível)
Django ORMPythonIntegrado ao Django. Simplicidade e produtividade
ActiveRecordRubyIntegrado ao Rails. Convenção sobre configuração
SequelizeNode.jsORM para Node.js com suporte a PostgreSQL, MySQL, SQLite
PrismaNode.js / TypeScriptORM moderno com schema declarativo e type safety
TypeORMTypeScriptSuporte a decorators e Active Record/Data Mapper
GORMGoORM mais popular do ecossistema Go

💡 Dica: O EF Core é o ORM mais utilizado no ecossistema .NET e é mantido ativamente pela Microsoft. Nas pesquisas do Stack Overflow Developer Survey, consistentemente aparece entre os ORMs mais populares globalmente.


EF Core vs Mini ORMs: Além da Performance

Quando se compara EF Core com Dapper, a conversa quase sempre gira em torno de performance: “Dapper é mais rápido”. E é verdade — o Dapper tem menos overhead por ser essencialmente um wrapper fino sobre ADO.NET. Mas focar apenas em microsegundos por query é perder de vista o que realmente importa em um projeto de software corporativo.

O Que o EF Core Oferece Que o Dapper Não Oferece

FuncionalidadeEF CoreDapper
Change TrackingAutomático — detecta o que mudou e gera UPDATE apenas das colunas alteradasManual — você escreve o UPDATE completo
MigrationsVersionamento do schema do banco de dados integradoInexistente — você gerencia DDL manualmente
LINQConsultas tipadas com verificação em compilaçãoSQL como string — erros só em runtime
Relacionamentos.Include() / .ThenInclude() — joins automáticosVocê escreve o JOIN e mapeia manualmente
Projeções.Select(x => new { ... }) — gera SQL otimizadoVocê escreve a projeção no SQL
TransaçõesSaveChanges() é transacional por padrãoVocê gerencia IDbTransaction manualmente
InterceptorsHooks cross-cutting (audit, soft delete, multi-tenancy)Inexistente
Abstração de bancoTroca de provider sem alterar código da aplicaçãoReescreve TODA query SQL

Desacoplamento Total: Vantagem Tática e Estratégica

Este é o ponto mais subestimado e mais importante. Com o EF Core, sua aplicação não conhece SQL. Ela conhece LINQ e entidades. O provider do EF Core traduz LINQ para o SQL específico do banco. Isso significa que:

Cenário tático (curto prazo): Sua empresa usa SQL Server em produção. O custo de licenciamento cresceu. A diretoria decide migrar para PostgreSQL. Com EF Core, você troca o provider (UseSqlServerUseNpgsql), ajusta as migrations e pronto. Com Dapper, você reescreve centenas ou milhares de queries SQL que usam sintaxe específica do SQL Server (TOP, NOLOCK, CROSS APPLY, etc.).

Cenário estratégico (longo prazo): A empresa vai expandir para multi-cloud. Alguns clientes exigem Oracle, outros PostgreSQL. Com EF Core, o mesmo código da aplicação roda em ambos — basta configurar o provider por tenant. Com Dapper, você mantém duas bases de código SQL ou usa um subset limitado de SQL ANSI que nenhum banco implementa completamente.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// A mesma aplicação rodando em bancos diferentes — zero alteração no código de negócio
// Basta trocar o provider no Startup/Program.cs

// SQL Server
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString));

// PostgreSQL
services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(connectionString));

// Oracle
services.AddDbContext<AppDbContext>(options =>
    options.UseOracle(connectionString));

// SQLite (para testes)
services.AddDbContext<AppDbContext>(options =>
    options.UseSqlite("Data Source=:memory:"));

ℹ️ Informação: O desacoplamento do banco não é um luxo técnico — é uma decisão de negócio. Empresas que acoplam sua aplicação ao SQL do banco ficam reféns de licenciamento, vendor lock-in e custo de migração. Com EF Core, a troca de banco é uma decisão de configuração, não de reescrita. Para ver como o EF Core funciona com diferentes bancos na prática, confira Paginação em APIs REST com C# e EF Core 8.

Diagrama de abstração do EF Core mostrando a aplicação C# com LINQ conectando-se a diferentes provedores de banco de dados PostgreSQL, SQL Server, Oracle e SQLite sem alteração de código


Por Que Fluent API e Nunca Data Annotations

O EF Core oferece duas formas de configurar o mapeamento entre entidades e tabelas: Data Annotations (atributos decorando as classes) e Fluent API (configuração em classes separadas). Este artigo usa exclusivamente Fluent API, e aqui está o porquê:

Data Annotations — O Problema

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// ❌ NÃO FAÇA ISSO — Data Annotations poluem o domínio
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

[Table("produtos")]
public class Produto
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    [Required]
    [MaxLength(200)]
    [Column("nome_produto")]
    public string Nome { get; set; } = string.Empty;

    [Column(TypeName = "decimal(18,2)")]
    public decimal Preco { get; set; }
}

O problema é claro: sua entidade de domínio está contaminada com detalhes de infraestrutura. A classe Produto agora depende de System.ComponentModel.DataAnnotations e System.ComponentModel.DataAnnotations.Schema — namespaces que são responsabilidade da camada de dados, não do domínio. Isso viola o princípio da Separação de Responsabilidades (SoC) e torna impossível ter um projeto Domain verdadeiramente independente.

Fluent API — A Solução

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// ✅ Entidade de domínio limpa — sem nenhuma referência ao EF Core
namespace MeuEcommerce.Domain.Entities;

public class Produto
{
    public int Id { get; private set; }
    public string Nome { get; private set; } = string.Empty;
    public decimal Preco { get; private set; }
    public bool Ativo { get; private set; } = true;
    public DateTime CriadoEm { get; private set; }

    public int CategoriaId { get; private set; }
    public Categoria Categoria { get; private set; } = null!;
    public ProdutoDetalhe? Detalhe { get; private set; }
    public ICollection<Tag> Tags { get; private set; } = [];
}

A configuração fica numa classe separada, no projeto de infraestrutura:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Infra.Data/Configurations/ProdutoConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using MeuEcommerce.Domain.Entities;

namespace MeuEcommerce.Infra.Data.Configurations;

public class ProdutoConfiguration : IEntityTypeConfiguration<Produto>
{
    public void Configure(EntityTypeBuilder<Produto> builder)
    {
        builder.ToTable("produtos");

        builder.HasKey(p => p.Id);

        builder.Property(p => p.Nome)
            .HasMaxLength(200)
            .IsRequired();

        builder.Property(p => p.Preco)
            .HasColumnType("decimal(18,2)");

        builder.Property(p => p.Ativo)
            .HasDefaultValue(true);

        builder.Property(p => p.CriadoEm)
            .HasDefaultValueSql("NOW()");

        builder.HasIndex(p => p.Nome);
    }
}

⚠️ Atenção: Data Annotations têm limitações técnicas reais: não suportam configuração de índices compostos, filtros globais, conversores de valor, shadow properties, owned types e vários outros recursos avançados. Fluent API é a única forma de acessar 100% das funcionalidades do EF Core.

Vantagens da Fluent API

  1. Domínio limpo — entidades são POCOs puros sem dependência do EF Core
  2. Separação de responsabilidades — mapeamento fica no projeto de infra, não no domínio
  3. Poder total — acesso a todas as funcionalidades do EF Core
  4. Organização — uma classe IEntityTypeConfiguration<T> por entidade
  5. Testabilidade — entidades podem ser instanciadas sem mock de EF Core
  6. Princípio aberto/fechado — adicionar configuração não altera a entidade

Configurando o DbContext

O DbContext é o ponto central do EF Core. Ele representa a sessão com o banco de dados e coordena o Change Tracking, as queries e o SaveChanges. Com Fluent API, o DbContext aplica automaticamente todas as configurações via ApplyConfigurationsFromAssembly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using Microsoft.EntityFrameworkCore;
using MeuEcommerce.Domain.Entities;

namespace MeuEcommerce.Infra.Data;

public class AppDbContext(DbContextOptions<AppDbContext> options)
    : DbContext(options)
{
    public DbSet<Produto> Produtos => Set<Produto>();
    public DbSet<Categoria> Categorias => Set<Categoria>();
    public DbSet<Tag> Tags => Set<Tag>();
    public DbSet<ProdutoDetalhe> ProdutoDetalhes => Set<ProdutoDetalhe>();
    public DbSet<Comentario> Comentarios => Set<Comentario>();
    public DbSet<Pagamento> Pagamentos => Set<Pagamento>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // Aplica TODAS as IEntityTypeConfiguration<T> do assembly automaticamente
        // Não precisa registrar uma por uma — adicione novas classes e elas são
        // detectadas automaticamente via reflection
        modelBuilder.ApplyConfigurationsFromAssembly(
            typeof(AppDbContext).Assembly);
    }
}

💡 Dica: Use o primary constructor do C# 12 (public class AppDbContext(DbContextOptions<AppDbContext> options)) para simplificar o construtor. E prefira DbSet<T> Produtos => Set<T>(); em vez de DbSet<T> Produtos { get; set; } — é mais limpo e seguro.


Todos os Tipos de Relacionamento com Fluent API

Vamos usar um domínio de e-commerce para demonstrar todos os tipos de relacionamento. Primeiro, as entidades do domínio (projeto Domain — sem referência ao EF Core):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
namespace MeuEcommerce.Domain.Entities;

public class Categoria
{
    public int Id { get; private set; }
    public string Nome { get; private set; } = string.Empty;
    public string? Descricao { get; private set; }
    public bool Ativa { get; private set; } = true;
    public DateTime CriadoEm { get; private set; }
    public ICollection<Produto> Produtos { get; private set; } = [];
}

public class Tag
{
    public int Id { get; private set; }
    public string Nome { get; private set; } = string.Empty;
    public ICollection<Produto> Produtos { get; private set; } = [];
}

public class ProdutoDetalhe
{
    public int Id { get; private set; }
    public string DescricaoCompleta { get; private set; } = string.Empty;
    public string? Especificacoes { get; private set; }
    public double PesoKg { get; private set; }
    public int ProdutoId { get; private set; }
    public Produto Produto { get; private set; } = null!;
}

public class Comentario
{
    public int Id { get; private set; }
    public string Texto { get; private set; } = string.Empty;
    public DateTime CriadoEm { get; private set; }
    public int ProdutoId { get; private set; }
    public Produto Produto { get; private set; } = null!;

    // Auto-referenciamento (resposta a outro comentário)
    public int? ComentarioPaiId { get; private set; }
    public Comentario? ComentarioPai { get; private set; }
    public ICollection<Comentario> Respostas { get; private set; } = [];
}

Relacionamento Um-para-Muitos (1:N)

O relacionamento mais comum. Uma Categoria possui muitos Produtos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class CategoriaConfiguration : IEntityTypeConfiguration<Categoria>
{
    public void Configure(EntityTypeBuilder<Categoria> builder)
    {
        builder.ToTable("categorias");
        builder.HasKey(c => c.Id);

        builder.Property(c => c.Nome)
            .HasMaxLength(100)
            .IsRequired();

        builder.Property(c => c.Descricao)
            .HasMaxLength(500);

        builder.Property(c => c.Ativa)
            .HasDefaultValue(true);

        builder.Property(c => c.CriadoEm)
            .HasDefaultValueSql("NOW()");

        builder.HasIndex(c => c.Nome)
            .IsUnique();

        // 1:N — Uma Categoria tem muitos Produtos
        builder.HasMany(c => c.Produtos)
            .WithOne(p => p.Categoria)
            .HasForeignKey(p => p.CategoriaId)
            .OnDelete(DeleteBehavior.Restrict); // Impede deletar categoria com produtos
    }
}

Relacionamento Um-para-Um (1:1)

Um Produto tem um ProdutoDetalhe (e vice-versa):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ProdutoDetalheConfiguration : IEntityTypeConfiguration<ProdutoDetalhe>
{
    public void Configure(EntityTypeBuilder<ProdutoDetalhe> builder)
    {
        builder.ToTable("produto_detalhes");
        builder.HasKey(d => d.Id);

        builder.Property(d => d.DescricaoCompleta)
            .HasMaxLength(4000)
            .IsRequired();

        builder.Property(d => d.Especificacoes)
            .HasColumnType("text");

        builder.Property(d => d.PesoKg)
            .HasColumnType("double precision");

        // 1:1 — Um ProdutoDetalhe pertence a exatamente um Produto
        builder.HasOne(d => d.Produto)
            .WithOne(p => p.Detalhe)
            .HasForeignKey<ProdutoDetalhe>(d => d.ProdutoId)
            .OnDelete(DeleteBehavior.Cascade);

        builder.HasIndex(d => d.ProdutoId)
            .IsUnique(); // Garante que cada produto tem no máximo 1 detalhe
    }
}

Relacionamento Muitos-para-Muitos (N:N)

Um Produto pode ter várias Tags, e uma Tag pode estar em vários Produtos. No EF Core 5+, o N:N funciona sem tabela intermediária explícita. No EF Core 8+, você pode configurar via Fluent API para ter controle total:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class ProdutoConfiguration : IEntityTypeConfiguration<Produto>
{
    public void Configure(EntityTypeBuilder<Produto> builder)
    {
        builder.ToTable("produtos");
        builder.HasKey(p => p.Id);

        builder.Property(p => p.Nome)
            .HasMaxLength(200)
            .IsRequired();

        builder.Property(p => p.Sku)
            .HasMaxLength(50)
            .IsRequired();

        builder.Property(p => p.Preco)
            .HasColumnType("decimal(18,2)");

        builder.Property(p => p.Ativo)
            .HasDefaultValue(true);

        builder.Property(p => p.CriadoEm)
            .HasDefaultValueSql("NOW()");

        builder.HasIndex(p => p.Sku)
            .IsUnique();

        // N:N — Produto ↔ Tag (tabela intermediária configurada pelo EF Core)
        builder.HasMany(p => p.Tags)
            .WithMany(t => t.Produtos)
            .UsingEntity<Dictionary<string, object>>(
                "produto_tags", // Nome da tabela intermediária
                right => right.HasOne<Tag>()
                    .WithMany()
                    .HasForeignKey("TagId")
                    .OnDelete(DeleteBehavior.Cascade),
                left => left.HasOne<Produto>()
                    .WithMany()
                    .HasForeignKey("ProdutoId")
                    .OnDelete(DeleteBehavior.Cascade),
                join =>
                {
                    join.HasKey("ProdutoId", "TagId");
                    join.ToTable("produto_tags");
                });
    }
}

💡 Dica: Se você precisa de propriedades adicionais na tabela intermediária (como DataAssociacao), crie uma entidade explícita ao invés de usar Dictionary<string, object>. O EF Core 8+ suporta isso nativamente com UsingEntity<ProdutoTag>().

Auto-Referenciamento

Um Comentário pode ser resposta a outro Comentário — é um relacionamento 1:N consigo mesmo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class ComentarioConfiguration : IEntityTypeConfiguration<Comentario>
{
    public void Configure(EntityTypeBuilder<Comentario> builder)
    {
        builder.ToTable("comentarios");
        builder.HasKey(c => c.Id);

        builder.Property(c => c.Texto)
            .HasMaxLength(2000)
            .IsRequired();

        builder.Property(c => c.CriadoEm)
            .HasDefaultValueSql("NOW()");

        // 1:N com Produto
        builder.HasOne(c => c.Produto)
            .WithMany()
            .HasForeignKey(c => c.ProdutoId)
            .OnDelete(DeleteBehavior.Cascade);

        // Auto-referenciamento — Comentário pode ter respostas
        builder.HasOne(c => c.ComentarioPai)
            .WithMany(c => c.Respostas)
            .HasForeignKey(c => c.ComentarioPaiId)
            .OnDelete(DeleteBehavior.Restrict) // Não deleta em cascata (mantém respostas)
            .IsRequired(false); // FK nullable — comentários raiz não têm pai

        builder.HasIndex(c => c.ProdutoId);
        builder.HasIndex(c => c.ComentarioPaiId);
    }
}

Herança no EF Core 8+: TPH, TPT e TPC

O EF Core suporta três estratégias para mapear hierarquias de herança para tabelas:

Entidades de Domínio (Herança)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
namespace MeuEcommerce.Domain.Entities;

public abstract class Pagamento
{
    public int Id { get; private set; }
    public decimal Valor { get; private set; }
    public DateTime PagoEm { get; private set; }
    public int PedidoId { get; private set; }
}

public class PagamentoCartao : Pagamento
{
    public string UltimosDigitos { get; private set; } = string.Empty;
    public string Bandeira { get; private set; } = string.Empty;
    public int Parcelas { get; private set; }
}

public class PagamentoPix : Pagamento
{
    public string ChavePix { get; private set; } = string.Empty;
    public string TransacaoId { get; private set; } = string.Empty;
}

public class PagamentoBoleto : Pagamento
{
    public string CodigoBarras { get; private set; } = string.Empty;
    public DateTime Vencimento { get; private set; }
}

Table Per Hierarchy (TPH) — Uma Tabela Para Toda a Hierarquia

Todas as classes na mesma tabela. O EF Core usa uma coluna discriminator para saber qual tipo é cada linha:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public class PagamentoTphConfiguration : IEntityTypeConfiguration<Pagamento>
{
    public void Configure(EntityTypeBuilder<Pagamento> builder)
    {
        builder.ToTable("pagamentos");
        builder.HasKey(p => p.Id);

        // TPH — Discriminator define o tipo concreto
        builder.HasDiscriminator<string>("tipo_pagamento")
            .HasValue<PagamentoCartao>("cartao")
            .HasValue<PagamentoPix>("pix")
            .HasValue<PagamentoBoleto>("boleto");

        builder.Property(p => p.Valor)
            .HasColumnType("decimal(18,2)");

        builder.Property(p => p.PagoEm)
            .HasDefaultValueSql("NOW()");
    }
}

Table Per Type (TPT) — Uma Tabela Por Classe

Cada classe tem sua própria tabela. A tabela base contém as propriedades comuns, e cada tabela derivada contém apenas as propriedades específicas + FK para a tabela base:

1
2
3
4
5
6
// TPT — Cada tipo tem sua própria tabela com FK para pagamentos
builder.ToTable("pagamentos"); // Tabela base

builder.Entity<PagamentoCartao>().ToTable("pagamentos_cartao");
builder.Entity<PagamentoPix>().ToTable("pagamentos_pix");
builder.Entity<PagamentoBoleto>().ToTable("pagamentos_boleto");

Table Per Concrete Type (TPC) — EF Core 7+

Cada classe concreta tem sua própria tabela completa (sem tabela base). Cada tabela contém todas as colunas — tanto da base quanto da derivada:

1
2
3
4
5
6
// TPC — Cada classe concreta é uma tabela independente (EF Core 7+)
builder.UseTpcMappingStrategy();

builder.Entity<PagamentoCartao>().ToTable("pagamentos_cartao");
builder.Entity<PagamentoPix>().ToTable("pagamentos_pix");
builder.Entity<PagamentoBoleto>().ToTable("pagamentos_boleto");
EstratégiaPrósContrasQuando usar
TPHPerformance (1 tabela, sem JOINs)Colunas nullable para tipos derivadosPoucos tipos, acesso frequente
TPTSchema normalizadoJOIN para cada query + pior performanceSchema limpo importa mais que performance
TPCSem JOINs, sem colunas nullableColunas duplicadas entre tabelas, UNION para queries polimórficasTipos raramente consultados juntos

⚠️ Atenção: Para a maioria dos cenários, TPH é a escolha recomendada pela Microsoft e pela comunidade. TPT tem problemas conhecidos de performance por exigir JOINs em toda query. TPC (EF Core 7+) é útil quando cada tipo concreto é consultado independentemente.


Funcionalidades Avançadas do EF Core 8+

Value Converters

Permitem transformar valores entre o C# e o banco de dados. Útil para persistir enums como string, criptografar campos ou converter tipos complexos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// Converter enum para string no banco
public enum StatusPedido
{
    Pendente,
    Aprovado,
    Enviado,
    Entregue,
    Cancelado
}

// Na configuração Fluent API:
builder.Property(p => p.Status)
    .HasConversion<string>() // Persiste o enum como string ("Pendente", "Aprovado", etc.)
    .HasMaxLength(20);

// Ou com converter customizado:
builder.Property(p => p.Status)
    .HasConversion(
        v => v.ToString().ToLowerInvariant(),
        v => Enum.Parse<StatusPedido>(v, ignoreCase: true));

Global Query Filters

Aplicam filtros automaticamente em todas as queries de uma entidade. Ideal para soft delete e multi-tenancy:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class ProdutoConfiguration : IEntityTypeConfiguration<Produto>
{
    public void Configure(EntityTypeBuilder<Produto> builder)
    {
        // Filtro global — queries NUNCA retornam produtos inativos
        // SELECT * FROM produtos WHERE ativo = true (automático em toda query)
        builder.HasQueryFilter(p => p.Ativo);
    }
}

// Para ignorar o filtro quando necessário (ex.: painel admin):
var todosProdutos = await context.Produtos
    .IgnoreQueryFilters()
    .ToListAsync();

Colunas JSON (EF Core 7+)

O EF Core 7+ permite mapear objetos complexos como colunas JSON no banco. Ideal para dados semi-estruturados:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Entidade com propriedade complexa
public class Produto
{
    public int Id { get; private set; }
    public string Nome { get; private set; } = string.Empty;
    public Dimensoes Dimensoes { get; private set; } = new();
}

public class Dimensoes
{
    public double Largura { get; set; }
    public double Altura { get; set; }
    public double Profundidade { get; set; }
    public string Unidade { get; set; } = "cm";
}

// Mapeamento Fluent API — Dimensoes como coluna JSON
builder.OwnsOne(p => p.Dimensoes, dim =>
{
    dim.ToJson(); // Armazena como JSON no banco
});

Coleções de Tipos Primitivos (EF Core 8+)

O EF Core 8 introduziu suporte nativo a coleções de tipos primitivos — sem necessidade de tabela separada ou Value Converter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Produto
{
    public int Id { get; private set; }
    public string Nome { get; private set; } = string.Empty;
    public List<string> ImagensUrls { get; private set; } = [];
    public List<string> PalavrasChave { get; private set; } = [];
}

// Mapeamento — armazenado como JSON array no banco
builder.Property(p => p.ImagensUrls)
    .HasColumnType("jsonb"); // PostgreSQL — ou 'nvarchar(max)' para SQL Server

builder.Property(p => p.PalavrasChave)
    .HasColumnType("jsonb");

ℹ️ Informação: As coleções de tipos primitivos do EF Core 8+ são armazenadas como arrays JSON nativos. No PostgreSQL, use jsonb para indexação eficiente. No SQL Server, o EF Core serializa automaticamente para nvarchar(max).


Dicas e Boas Práticas

  1. Uma IEntityTypeConfiguration<T> por entidade — nunca configure múltiplas entidades no OnModelCreating. Use ApplyConfigurationsFromAssembly() para detecção automática.

  2. Sempre configure OnDelete explicitamente — o comportamento padrão varia entre providers. Seja explícito com DeleteBehavior.Restrict, .Cascade ou .SetNull para evitar surpresas.

  3. Use private set; nas entidades — proteja o estado interno. Entidades de domínio devem ser modificadas apenas por métodos de negócio, não por setters públicos.

  4. Configure índices para FKs e colunas de busca — o EF Core NÃO cria índices automaticamente para foreign keys em todos os providers. Use builder.HasIndex(p => p.CategoriaId).

  5. Prefira AsNoTracking() para queries de leitura — se você não vai modificar os dados, desative o Change Tracking para ganhar performance significativa.

  6. Nunca exponha o DbContext diretamente na API — use o padrão Repository ou serviços intermediários. O DbContext é infraestrutura.

  7. Valide always com HasMaxLength() — sem HasMaxLength, o EF Core cria colunas nvarchar(max) / text, que prejudicam performance e impossibilitam indexação.

  8. Configure HasDefaultValueSql() para colunas de data — use expressões do banco (NOW(), GETUTCDATE()) para que as datas sejam geradas pelo servidor, não pela aplicação.


Conclusão

O Entity Framework Core 8+ com Fluent API oferece muito mais do que um simples mapeamento objeto-relacional. Ele entrega desacoplamento total do banco de dados — uma vantagem que transcende o técnico e impacta diretamente o negócio. Enquanto micro ORMs como o Dapper te prendem a SQL específico de um vendor, o EF Core permite que sua aplicação fale LINQ e deixe o provider traduzir para o dialeto do banco.

Ao usar exclusivamente Fluent API com IEntityTypeConfiguration<T>, suas entidades de domínio ficam limpas, testáveis e livres de qualquer dependência de infraestrutura. Os relacionamentos (1:1, 1:N, N:N, auto-referenciamento), as estratégias de herança (TPH, TPT, TPC) e as funcionalidades avançadas (Value Converters, Global Query Filters, colunas JSON, coleções primitivas) tornam o EF Core uma ferramenta completa para qualquer cenário de acesso a dados.

Para aprofundar no gerenciamento de Migrations em projetos multi-camada com dotnet secrets e scaffolding de bancos existentes, confira o artigo dedicado EF Core Migrations: Multi-Projeto, Secrets e Scaffolding. E se performance é sua preocupação, veja como o EF Core se comporta em cenários de alta carga em Gargalo em Banco de Dados com C# e EF Core.


Leia Também


Referências