O dia que o EF Core nos traiu !!! (ou não!!!)
    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.

"É como se nada tivesse mudado... mas tudo quebrou."



 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<Pedido> Pedidos => Set<Pedido>();

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.

Considerações Finais

O EF Core não é perigoso.
O EF Core não configurado é.

O momento em que você:

Remove os padrões
Torna as decisões explícitas
Trata o mapeamento como arquitetura


O EF Core se torna uma das ferramentas mais poderosas do ecossistema .NET.

Não é mágica.
Não é mistério.
Apenas engenharia.


A mensagem principal é profundamente filosófica sobre engenharia de software:

"Não delegue decisões de arquitetura ao framework. Assuma a propriedade (ownership) da sua configuração."

Mais especificamente:

O EF Core não é seu inimigo - ele apenas segue as regras (ou falta delas) que você fornece.

Configuração implícita = Risco implícito - Quando você permite que o EF Core "adivinhe" como mapear seu domínio, você está abrindo mão de decisões críticas sobre comportamento do sistema, segurança de dados e integridade do negócio.

Configuração é arquitetura - Como suas entidades mapeiam para o banco não é um detalhe técnico menor, mas uma decisão arquitetural que define invariantes de negócio, regras de exclusão, relacionamentos e consistência de dados.

Boring is beautiful - O objetivo ideal não é ter um ORM "mágico" ou "inteligente", mas sim um que seja previsível, explícito e, portanto, "chato" - onde não há surpresas em produção.

A responsabilidade é sua - Questões como "devo permitir exclusão em cascata?" ou "quantas casas decimais preciso para preços?" são decisões de negócio que o framework não pode (e não deve) tomar por você.

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:


José Carlos Macoratti