|
Este artigo procurar alertar desenvolvedores .NET sobre os riscos de usar o EF Core sem configuração explícita, mostrando como padrões implícitos podem causar bugs catastróficos em produção. |
A
implantação foi limpa.
Nenhum alerta.
Nenhum teste falhando.
Nenhum
aviso durante a migração.
Ainda assim, cinco minutos depois, a produção
começou a se comportar... de forma diferente.![]()
Pedidos estavam
desaparecendo.
Registros relacionados sumiram.
Uma operação de exclusão
removeu muito mais do que o pretendido.
E quer saber a parte mais aterrorizante ?
Ninguém tocou no código de exclusão.
O culpado silencioso
Após horas de logs, rollbacks e comparação
de commits, encontramos o culpado:
Uma linha código :
public DbSet |
Não estava errada.
Não era nova.
Não
era suspeita.
Mas o que estava por trás dela era o problema...![]()
O EF
Core havia decidido silenciosamente:
Como os relacionamentos se comportam
O que acontece ao excluir
Como as chaves estrangeiras se propagam em cascata
Quais colunas são anuláveis
Como as tabelas devem ser
Ninguém
configurou explicitamente nada disso.
Nós deixamos o EF Core adivinhar.
E o EF Core fez exatamente o que permitimos que ele fizesse.
Fomos enganados pela ilusão da simplicidade.
O DbSet não é o problema.
O problema é confiar que o EF Core entenda o seu domínio melhor do que você.
A maioria dos tutoriais começa assim:
builder.Services.AddDbContext<ContextoApp>(opcoes => { opcoes.UseSqlServer(stringDeConexao); }); |
E eles param por aí.
É aí que a
ilusão começa.
Porque o que o EF Core realmente entende com isso é:
"Faça o que você achar melhor."
O que você acha que está dizendo :
“Use SQL Server com essa string de conexão.”
O que
o EF Core realmente entende : “Tenho pouquíssimas informações.Vou
completar o resto com convenções.”
E o EF Core responde feliz:
"Claro, vou assumir exclusões em cascata"
"Claro, vou inferir
relacionamentos"
"Claro, vou transformar strings em nvarchar(max)"
"Claro,
vou gerar chaves para você"
Nada disso está errado. Mas nada disso é
intencional.
Por que isso se torna perigoso em escala.
No início, o
EF Core parece amigável.
Depois, ele se torna... opinativo.
As
migrações ficam enormes
Refatorar parece arriscado
Diferenças no banco de
dados são assustadoras
Bugs em produção parecem misteriosos
E a equipe
começa a dizer coisas como:
"Não mexa no DbContext."
"O EF faz coisas
estranhas."
"Está funcionando... só não pergunte por quê."
Onde o problema realmente nasce
O
AddDbContext não
descreve o domínio. Ele só registra infraestrutura.
Sem configuração
explícita:
❌ o domínio não está documentado
❌ o comportamento não é
intencional
❌ o banco vira um efeito colateral
Você não decidiu:
regras de exclusão
nulabilidade
limites de string
obrigatoriedade
integridade relacional
O EF decidiu por
ausência de decisão sua.
Isso acaba se tornando medo.
O ponto
da virada
Este é o
momento que todo desenvolvedor sênior eventualmente atinge:
O EF Core não é
um problema de ORM.
É um problema de configuração.
O EF Core não nos
traiu.
Nós abandonamos a tomada de decisão e delegamos isso para o EF Core.
Configuração é arquitetura — não infraestrutura
Vamos deixar isso claro:
A forma como seu domínio mapeia para o
banco de dados é uma decisão de design.
"Não é ruído de infraestrutura.
Não é código repetitivo.
Não é "para depois".
Ela define:
O que
pode ser excluído
O que deve existir
O que os relacionamentos significam
Quais invariantes são impostas
Se você não definir essas regras
explicitamente, o EF Core definirá.
A
primeira regra do melhor caminho
Nunca configure o EF Core implicitamente
quando seu projeto importa.
Sem atributos.
Sem convenções que você não
entende.
Sem "o EF vai descobrir".
Nós tornamos cada regra
visível.
Passo 1 — A estrutura de projeto que restaura a sanidade
Em vez disso:
App └── ContextoApp.cs (tudo dentro) |
Fazemos isso:
MeuApp.Infraestrutura └── Persistencia ├── ContextoApp.cs └── Configuracoes ├── ConfiguracaoPedido.cs ├── ConfiguracaoCliente.cs └── ConfiguracaoProduto.cs |
Cada entidade possui seu mapeamento.
Cada decisão tem um lar.
Passo 2 —
O DbContext silencioso
Seu DbContext
deve parecer... chato.
Isso é um elogio.
public sealed class ContextoApp : DbContext { public ContextoApp(DbContextOptions<ContextoApp> opcoes) : base(opcoes) { } public DbSet<Pedido> Pedidos => Set<Pedido>(); public DbSet<Cliente> Clientes => Set<Cliente>(); protected override void OnModelCreating(ModelBuilder construtorModelo) { construtorModelo.ApplyConfigurationsFromAssembly( typeof(ContextoApp).Assembly); } } |
Sem caos fluent.
Sem comportamento
oculto.
Sem surpresas.
O DbContext orquestra, ele não decide.
Passo 3 - Deixe o domínio respirar
Agora o domínio.
Sem atributos do
EF.
Sem vazamento de persistência.
Sem concessões.
public sealed class Pedido { public Guid Id { get; private set; } public DateTime DataCriacao { get; private set; } private readonly List<ItemPedido> _itens = new(); public IReadOnlyCollection<ItemPedido> Itens => _itens; private Pedido() { }
public Pedido(DateTime dataCriacao) { Id = Guid.NewGuid(); DataCriacao = dataCriacao; } } |
O domínio não se importa com:
Tabelas
Colunas
Chaves estrangeiras
Cascatas
Esse não é o trabalho dele.
Agora fazemos as perguntas reais:
Um Pedido deve ser excluído?
Seus itens devem ser excluídos?
O banco de
dados deve impor isso?
Ou o domínio deve impor?
O EF Core não
responderá isso corretamente para você.
Você deve decidir.
E essa
decisão vive na configuração.
Por que a Parte 1 pára aqui ?
Porque
este é o ponto de virada.
Antes da configuração, o EF Core parece mágica.
Depois da configuração, o EF Core parece uma ferramenta.
Na Parte 2, nós
configuraremos entidades explicitamente
Eliminaremos padrões
perigosos
Controlaremos o comportamento de exclusão
Definiremos precisão,
chaves e restrições
Mostraremos por que a maioria das exclusões em cascata
são erros
E na Parte 3, nós:
Adicionaremos convenções globais
Configuração baseada em ambiente
Disciplina de migração
Testes de
mapeamentos
E o modelo mental final que torna o EF Core chato — da melhor
maneira.
Eliminando Padrões Perigosos (Antes que eles Matem seus Dados)
O EF Core não
excluiu os dados.
Sua configuração permitiu isso.
Após o
incidente, a equipe fez o que toda equipe faz:
Leu arquivos de migração
linha por linha
Adicionou logging
Culpou o SQL Server
Culpou o EF Core
Culpou a "mágica"
Mas a verdade veio à tona lentamente — e dolorosamente.
O EF Core fez exatamente o que nunca dissemos para ele não fazer.
O recurso mais perigoso do EF Core
Vamos dizer isso em voz alta:
As convenções do EF Core
são otimizadas para demos, não para sistemas de produção.
As
convenções existem para reduzir o atrito — não para codificar regras de negócio.
E, no entanto, muitos sistemas dependem inteiramente delas.
O
primeiro assassino silencioso: Exclusões em Cascata
Por
padrão, o EF Core assume isto:
"Se algo depende de outra coisa... excluir
o pai provavelmente exclui os filhos."
Essa suposição destruiu mais
dados em produção do que SQL ruim jamais destruiu.
O relacionamento
"parece inofensivo"
public sealed class Pedido { public Guid Id { get; private set; } public Cliente Cliente { get; private set; } } public sealed class Cliente { public Guid Id { get; private set; } } |
Sem configuração.
O EF Core infere:
Relacionamento obrigatório -> Exclusão em cascata
Então, quando
alguém exclui um Cliente... Todos os pedidos somem.
Isso não é uma decisão técnica
Esta é uma decisão de
negócio.
Os pedidos devem sobreviver à exclusão de um cliente?
A
exclusão sequer deve ser permitida?
Deve ser uma exclusão lógica
(soft-delete)?
Deve ser bloqueada?
O EF Core não pode responder isso.
A obrigação é da sua equipe..
O melhor caminho:
Configuração explícita de relacionamento
es
public sealed class ConfiguracaoPedido : IEntityTypeConfiguration<Pedido> { public void Configure(EntityTypeBuilder<Pedido> builder) { builder.ToTable("Pedidos"); builder.HasKey(p => p.Id); builder.Property(p => p.DataCriacao) .IsRequired(); builder.HasOne<Cliente>() .WithMany() .HasForeignKey("ClienteId") .OnDelete(DeleteBehavior.Restrict); } } |
Agora a exclusão se torna intencional.
O EF Core não adivinha mais.
Por que Restrict é seu
padrão mais seguro
Vamos ser diretos:
Exclusões em cascata raramente
são o que você quer em sistemas reais.
Elas:
Ocultam efeitos
colaterais
Tornam as exclusões imprevisíveis
Quebram a auditabilidade
Ignoram regras de domínio
Uma regra melhor: Se a exclusão importa,
force a aplicação a decidir.
DeleteBehavior.Restrict faz
exatamente isso.
O segundo assassino
silencioso: Propriedades sombra
O EF
Core adora propriedades sombra.
Você não as vê.
Você não as digita.
Mas elas existem.
builder.HasOne<Cliente>() .WithMany(); |
O EF Core cria:
ClienteId
UNIQUEIDENTIFIER NOT NULL
Mas ela não está no seu domínio.
Agora:
Você não pode validá-la
Você não pode raciocinar sobre ela
Você não pode protegê-la
O melhor caminho: Torne os
relacionamentos visíveis
Mesmo que seu domínio não exponha
propriedades de navegação, o mapeamento deve ser explícito.
builder.Property<Guid>("ClienteId") .IsRequired(); |
Agora:
A coluna existe intencionalmente
O relacionamento está
documentado
O banco de dados conta a verdade
O terceiro assassino:
Definições implícitas de colunas
Por padrão, o EF Core diz:
string
→ nvarchar(max)
decimal → (18,2)
DateTime → padrão do provedor
Isso
parece estar ok...
Até que quando não for...
Uma falha do mundo real
Um sistema de preços:
public decimal Preco { get; private set; }
|
O EF Core mapeia para (18,2).
Seis meses depois:
Descontos
precisam de 4 casas decimais
Bugs de arredondamento aparecem
Diferenças
financeiras explodem
O melhor caminho: Seja explícito
builder.Property(p => p.Preco) .HasPrecision(18, 4) .IsRequired(); |
Agora:
A precisão é intencional
As migrações são estáveis
O
financeiro para de gritar
Chaves: pare de deixar o EF Core
decidir
O EF Core define como padrão:
Chaves substitutas
(surrogate keys)
Colunas de identidade
Valores gerados automaticamente
Às vezes isso pode parecer ok.
Às vezes está errado.
Exemplo: IDS gerados pelo domínio
public sealed class Pedido { public Guid Id { get; private set; } public Pedido() { Id = Guid.NewGuid(); } } |
Se você não configurar isso... O EF Core assume que ele gera o ID.
Agora:
Inserções falham
O rastreamento (tracking) se comporta de forma
estranha
A depuração se torna dolorosa
A correção:
builder.Property(p => p.Id) .ValueGeneratedNever(); |
Agora o EF Core respeita o domínio.
A mudança de mentalidade (isso muda
tudo)
A
configuração do EF Core não é sobre mapear tabelas.
É sobre proteger a
intenção.
Toda configuração responde a uma pergunta:
Isso deve
existir?
Isso deve excluir?
Isso deve ser anulável?
Isso deve ser
gerado?
Se a resposta não é explícita, o EF Core inventa uma.
Por que as migrações de repente se tornam chatas
Após a
configuração explícita:
As migrações param de explodir
Mudanças de
esquema se tornam previsíveis
Revisões se tornam fáceis
O ruído de
diferenças desaparece
E o mais importante...
A produção para de
surpreender você.
Para onde vamos na Parte 3
Até agora nós:
Removemos padrões perigosos
Assumimos o controle dos relacionamentos
Tornamos as restrições intencionais
Na Parte 3, iremos além:
Convenções de configuração global
Imposição de consistência automaticamente
Configuração do EF Core consciente do ambiente
Disciplina de migração
Testes de mapeamentos antes da produção
E o modelo mental final que torna o
EF Core chato — para sempre.
Tornando o EF Core Chato (e Esse é o Objetivo)
"A melhor configuração de ORM é aquela sobre a qual você nunca mais pensa."
Neste ponto da história, a equipe fez algo raro: Eles pararam de
lutar contra o EF Core.
Não porque o EF Core "venceu" — mas porque eles
finalmente assumiram o controle dele.
O EF Core
não é:
Seu modelo de domínio ❌
Suas regras de negócio ❌
Sua
arquitetura ❌
O EF Core é: Um detalhe de persistência que deve
obedecer ao seu sistema — não moldá-lo.
Uma vez que essa mentalidade
esteja ativa, tudo muda.
Passo 1: Centralize a configuração (sem mais
caos)
Configuração espalhada é configuração que você esquecerá.
A primeira decisão estrutural:
Toda entidade deve ter exatamente uma
classe de configuração.
Estrutura de pasta
Infraestrutura └── Persistencia ├── ContextoAplicacao.cs ├── Configuracoes │ ├── ConfiguracaoPedido.cs │ ├── ConfiguracaoCliente.cs │ └── ConfiguracaoProduto.cs |
Nada no Domínio.
Nada na Camada de Aplicação.
Preocupações de
persistência ficam isoladas.
Passo 2: Aplique as configurações
automaticamente
Sem registro manual.
Sem mapeamentos
esquecidos.
public sealed class ContextoAplicacao : DbContext { public ContextoAplicacao(DbContextOptions opcoes) : base(opcoes) { } protected override void OnModelCreating(ModelBuilder construtorModelo) { construtorModelo.ApplyConfigurationsFromAssembly( typeof(ContextoAplicacao).Assembly); } } |
Agora:
Toda IEntityTypeConfiguration<T> é aplicada
Sem erro humano
Sem mapeamentos esquecidos
Passo 3:
Convenções globais (consistência sem repetição)
A equipe notou
algo: Toda entidade tinha:
Uma chave primária
Carimbos de
data/hora de criação
Às vezes carimbos de atualização
Em vez de
repetir essa lógica...
Eles a centralizaram.
| public abstract class Entidade { public Guid Id { get; protected set; } } |
Configuração global do EF Core
Exemplo:
protected override void ConfigureConventions( ModelConfigurationBuilder builderConfiguracao) { builderConfiguracao.Properties<string>() .HaveMaxLength(200); builderConfiguracao.Properties<decimal>() .HavePrecision(18, 4); } |
Agora:
Strings nunca são nvarchar(max)
Decimais são sempre seguros
Sem duplicação por entidade
Passo 4: Impondo invariantes no nível
do banco de dados
Aqui está a verdade desconfortável:
Se o
banco de dados permite dados inválidos, dados inválidos existirão.
O EF
Core deve impor invariantes — não apenas o domínio.
Exemplo: Regras de
negócio únicas
builder.HasIndex(c => c.Email) .IsUnique(); |
Isso faz duas coisas:
Protege o banco de dados
Protege você de
condições de corrida (race conditions)
Nenhuma quantidade de validação
substitui um índice único.
Passo 5: Exclusões lógicas (soft
deletes) feitas corretamente
A equipe havia aprendido esta lição
da maneira difícil:
"Excluir dados é fácil. Recuperar a confiança
não é."
Modelo de domínio:
| builder.HasQueryFilter(x => !x.EstaExcluido); |
Agora:
Linhas excluídas permanecem no banco de dados
As consultas as
excluem automaticamente
Auditoria se torna possível
Passo 6: Transações pertencem acima do EF Core
O EF Core
rastreia mudanças.
Ele não define transações de negócio.
Esse é o
trabalho da camada de aplicação.
Transação no estilo Unidade de
Trabalho
public async Task ExecutarAsync(Func<Task> acao) { using var transacao = await _contexto.Database.BeginTransactionAsync(); try { await acao(); await _contexto.SaveChangesAsync(); await transacao.CommitAsync(); } catch { await transacao.RollbackAsync(); throw; } } |
Agora:
O EF Core é previsível
As operações de negócio são atômicas
Falhas são explícitas
Passo 7: Disciplina de migrações (isso
salva equipes)
A regra que adotaram: Toda migração deve ser
revisada como código de produção.
Nenhum SQL gerado automaticamente é
mesclado cegamente.
Boas práticas que eles impuseram
Sem migrações
vazias
Sem mudanças destrutivas sem justificativa
Sem migrações em
produção sem testes secos (dry runs)
Sempre inspecione o SQL gerado
O
EF Core lhe dá poder.
A disciplina o mantém seguro.
Passo
8: Testando seus mapeamentos (sim, sério)
A maioria das equipes
nunca testa os mapeamentos do EF Core.
Isso é um erro.
Exemplo de
teste de mapeamento:
[Fact] public void Mapeamento_Pedido_Eh_Valido() { using var contexto = CriarContexto(); contexto.Database.EnsureCreated(); var pedido = new Pedido(/* ... */); contexto.Add(pedido); contexto.SaveChanges(); } |
Isso captura:
Restrições inválidas
Campos obrigatórios faltando
Configurações quebradas
Antes da produção.
O modelo mental
final (esta é a chave)
Aqui está como a equipe agora pensa sobre
o EF Core:
O Domínio define o comportamento
A Aplicação orquestra os
casos de uso
A Infraestrutura traduz a intenção para armazenamento
O EF
Core é explícito, chato e obediente
É isso.
Por que o "EF
Core chato" é a vitória definitiva
Quando o EF Core é chato:
Bugs desaparecem
Migrações se estabilizam
Novos desenvolvedores se
integram mais rápido
Incidentes em produção caem
E o mais
importante...
Você para de temer seu banco de dados.
Um lição para levar de toda esta jornada, é esta:
Não pergunte ao EF
Core o que ele quer. Diga a ele exatamente o que você quer dizer.
É
assim que você constrói sistemas que duram.
E estamos conversados
![]()
"A esperança dos justos é alegria, mas a expectação dos perversos perecerá."
Provérbios 10:28
Referências: