Introdução

Se você já configurou EF Core Migrations em um projeto simples com um único csproj, sabe que é razoavelmente direto: dotnet ef migrations add, dotnet ef database update e pronto. Mas a realidade de projetos corporativos é outra. Sua solução tem múltiplos projetos — API, Domain, IoC, Infra.Data.PostgreSQL, Infra.Bus, Infra.Http, Infra.Dto — e o DbContext não vive no mesmo projeto que o entry point. Agora adicione dotnet secrets para gerenciar connection strings de forma segura, a necessidade de fazer scaffolding de bancos legados que já existem, e um time com 5 ou 10 desenvolvedores criando migrations simultaneamente.

Neste artigo, vamos resolver cada um desses problemas na prática. Vamos configurar migrations em uma solução multi-camada real, usar dotnet user-secrets para connection strings (sem senhas em appsettings.json), fazer reverse engineering de bancos existentes com Scaffold-DbContext, e estabelecer estratégias concretas para evitar conflitos de migrations em times grandes. Se você ainda não conhece o mapeamento avançado com Fluent API que gera essas migrations, recomendo antes a leitura de EF Core 8 Fluent API: Mapeamento, ORM e Desacoplamento.

📦 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.


Estrutura do Projeto Multi-Camada

Antes de falar de migrations, é essencial entender a estrutura do projeto. É uma solução .NET 8 com separação de responsabilidades em múltiplos csproj:

 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
MeuEcommerce.sln
├── src/
│   ├── MeuEcommerce.Api/                    # Entry point — ASP.NET Core Web API
│   │   ├── Program.cs
│   │   ├── Controllers/
│   │   └── appsettings.json
│   │
│   ├── MeuEcommerce.Domain/                 # Entidades e interfaces — SEM dependência de EF Core
│   │   ├── Entities/
│   │   │   ├── Produto.cs
│   │   │   ├── Categoria.cs
│   │   │   └── ...
│   │   └── Interfaces/
│   │       └── IProdutoRepository.cs
│   │
│   ├── MeuEcommerce.IoC/                    # Injeção de dependência — registra tudo
│   │   └── DependencyInjection.cs
│   │
│   ├── MeuEcommerce.Infra.Data.PostgreSQL/  # EF Core + Migrations (DbContext vive AQUI)
│   │   ├── AppDbContext.cs
│   │   ├── Configurations/
│   │   │   ├── ProdutoConfiguration.cs
│   │   │   └── CategoriaConfiguration.cs
│   │   ├── Repositories/
│   │   │   └── ProdutoRepository.cs
│   │   ├── Migrations/                      # Migrations geradas aqui
│   │   └── DesignTimeDbContextFactory.cs    # Factory para o EF CLI
│   │
│   ├── MeuEcommerce.Infra.Bus/             # Mensageria (RabbitMQ, Azure Service Bus)
│   ├── MeuEcommerce.Infra.Http/            # HttpClient para APIs externas
│   └── MeuEcommerce.Infra.Dto/             # DTOs compartilhados
└── tests/
    └── MeuEcommerce.Tests/

Ponto crítico: O DbContext vive em Infra.Data.PostgreSQL, mas o entry point (Program.cs) vive em Api. O EF Core CLI precisa de ambos para funcionar — ele precisa do DbContext para gerar migrations e do entry point para resolver connection strings e dependências.

ℹ️ Informação: Essa estrutura segue o padrão de Arquitetura em Camadas / Clean Architecture onde o Domain não tem dependência de infraestrutura. Se você quer entender os padrões arquiteturais por trás dessa organização, veja Arquitetura de Software: GoF, Padrões e Microsserviços.


Pré-requisitos

  • .NET 8.0 SDK ou superior
  • dotnet-ef tool (EF Core CLI):
1
2
3
4
5
6
7
dotnet tool install --global dotnet-ef

# Ou atualizar para última versão
dotnet tool update --global dotnet-ef

# Verificar instalação
dotnet ef --version
  • Pacotes NuGet no projeto Infra.Data.PostgreSQL:
1
2
3
4
5
cd src/MeuEcommerce.Infra.Data.PostgreSQL

dotnet add package Microsoft.EntityFrameworkCore --version 8.0.*
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 8.0.*
dotnet add package Microsoft.EntityFrameworkCore.Design --version 8.0.*

⚠️ Atenção: O pacote Microsoft.EntityFrameworkCore.Design é obrigatório para que o CLI do EF Core funcione. Ele pode ser adicionado no projeto de dados OU no projeto de startup. Se estiver apenas no startup, use --startup-project ao rodar os comandos.


Configurando Migrations em Multi-Projeto

O Problema

Ao executar dotnet ef migrations add sem configuração adicional, o CLI tenta encontrar o DbContext e as dependências no projeto atual. Num projeto multi-camada, isso falha com erros como:

1
2
Unable to create a 'DbContext' of type 'AppDbContext'.
The exception 'Unable to resolve service for type...' was thrown.

O CLI precisa de duas informações:

  1. Onde está o DbContext (--project) — no Infra.Data.PostgreSQL
  2. Onde está o entry point (--startup-project) — no Api (para resolver DI e connection string)

Solução 1: Parâmetros do CLI (Rápido)

1
2
3
4
5
6
7
8
9
# A partir da raiz da solução
dotnet ef migrations add InitialCreate \
  --project src/MeuEcommerce.Infra.Data.PostgreSQL \
  --startup-project src/MeuEcommerce.Api \
  --output-dir Migrations

dotnet ef database update \
  --project src/MeuEcommerce.Infra.Data.PostgreSQL \
  --startup-project src/MeuEcommerce.Api

Funciona, mas é verboso. A cada migration você precisa lembrar dos caminhos.

Solução 2: IDesignTimeDbContextFactory (Recomendado)

Crie uma factory no projeto Infra.Data.PostgreSQL que o CLI usa em design-time — sem depender do Program.cs da API:

 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
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;

namespace MeuEcommerce.Infra.Data.PostgreSQL;

/// <summary>
/// Factory usada APENAS pelo EF Core CLI (dotnet ef migrations add/update).
/// Em runtime, o DbContext é resolvido pela DI configurada no IoC.
/// </summary>
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
{
    public AppDbContext CreateDbContext(string[] args)
    {
        // Busca a connection string do user-secrets ou environment variables
        var configuration = new ConfigurationBuilder()
            .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json", optional: true)
            .AddUserSecrets<DesignTimeDbContextFactory>(optional: true)
            .AddEnvironmentVariables()
            .Build();

        var connectionString = configuration.GetConnectionString("DefaultConnection")
            ?? throw new InvalidOperationException(
                "Connection string 'DefaultConnection' não encontrada. " +
                "Configure via 'dotnet user-secrets set' ou variável de ambiente.");

        var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
        optionsBuilder.UseNpgsql(connectionString, npgsql =>
        {
            npgsql.MigrationsAssembly(
                typeof(DesignTimeDbContextFactory).Assembly.FullName);
        });

        return new AppDbContext(optionsBuilder.Options);
    }
}

Com essa factory, o CLI encontra o DbContext diretamente:

1
2
3
4
5
# Agora basta apontar para o projeto de dados — sem --startup-project
cd src/MeuEcommerce.Infra.Data.PostgreSQL

dotnet ef migrations add InitialCreate
dotnet ef database update

💡 Dica: A IDesignTimeDbContextFactory é usada apenas pelo CLI do EF Core (em design-time). Em runtime, o DbContext continua sendo resolvido pela injeção de dependências configurada no projeto IoC. As duas vias coexistem sem conflito.


dotnet user-secrets para Connection Strings

Nunca coloque connection strings com senhas em appsettings.json (que vai para o Git). Use dotnet user-secrets para armazenar dados sensíveis localmente.

Configuração Inicial

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# No projeto que tem a IDesignTimeDbContextFactory (Infra.Data.PostgreSQL)
cd src/MeuEcommerce.Infra.Data.PostgreSQL
dotnet user-secrets init

# Definir a connection string
dotnet user-secrets set "ConnectionStrings:DefaultConnection" \
  "Host=localhost;Port=5432;Database=meuecommerce;Username=postgres;Password=minha-senha-local"

# Verificar secrets armazenados
dotnet user-secrets list

No projeto Api (entry point), faça o mesmo:

1
2
3
4
5
cd src/MeuEcommerce.Api
dotnet user-secrets init

dotnet user-secrets set "ConnectionStrings:DefaultConnection" \
  "Host=localhost;Port=5432;Database=meuecommerce;Username=postgres;Password=minha-senha-local"

No appsettings.json — Sem Senha

O appsettings.json versionado no Git fica sem dados sensíveis:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "ConnectionStrings": {
    "DefaultConnection": ""
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information"
    }
  }
}

Em runtime, o builder.Configuration do ASP.NET Core carrega os user-secrets automaticamente quando IsDevelopment() é true. Em produção, use variáveis de ambiente ou Azure Key Vault.

Executando Migrations com Secrets

Com a IDesignTimeDbContextFactory configurada para ler AddUserSecrets, os comandos do EF Core CLI funcionam direto:

1
2
3
4
5
6
7
8
cd src/MeuEcommerce.Infra.Data.PostgreSQL

# A factory lê a connection string dos user-secrets
dotnet ef migrations add AdicionarTabelaProdutos
dotnet ef database update

# Para ver o SQL que será gerado (sem executar):
dotnet ef migrations script --idempotent

⚠️ Atenção: Cada desenvolvedor do time deve executar dotnet user-secrets set na máquina dele com as credenciais do banco local. Os user-secrets são armazenados em %APPDATA%\Microsoft\UserSecrets\ (Windows) ou ~/.microsoft/usersecrets/ (Linux/macOS) e nunca vão para o Git.


Scaffolding: Banco de Dados Existente (Reverse Engineering)

Quando você precisa integrar com um banco de dados existente (legado), o EF Core pode gerar automaticamente as entidades e configurações a partir do schema existente. Isso é chamado de scaffolding ou reverse engineering.

Comando Básico

1
2
3
4
5
6
7
8
dotnet ef dbcontext scaffold \
  "Host=localhost;Port=5432;Database=sistema_legado;Username=postgres;Password=senha" \
  Npgsql.EntityFrameworkCore.PostgreSQL \
  --project src/MeuEcommerce.Infra.Data.PostgreSQL \
  --output-dir Entities \
  --context-dir . \
  --context LegadoDbContext \
  --force

Opções Essenciais do Scaffold

ParâmetroDescrição
--output-dir EntitiesOnde gerar as classes de entidade
--context-dir .Onde gerar o DbContext
--context LegadoDbContextNome da classe DbContext
--schema publicScaffold apenas as tabelas de um schema específico
--table produtos --table categoriasScaffold apenas tabelas específicas
--no-onconfiguringNão gera OnConfiguring com a connection string hardcoded
--data-annotationsUsa Data Annotations (default é Fluent API — queremos Fluent API!)
--forceSobrescreve arquivos existentes
--no-pluralizeNão pluraliza nomes de DbSet

Scaffold com Fluent API (Recomendado)

Por padrão, o scaffold já usa Fluent API (não Data Annotations). Para ter o máximo de controle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
dotnet ef dbcontext scaffold \
  "Host=localhost;Port=5432;Database=sistema_legado;Username=postgres;Password=senha" \
  Npgsql.EntityFrameworkCore.PostgreSQL \
  --project src/MeuEcommerce.Infra.Data.PostgreSQL \
  --output-dir Entities/Legado \
  --context-dir . \
  --context LegadoDbContext \
  --no-onconfiguring \
  --no-pluralize \
  --force

💡 Dica: Após o scaffold, mova as entidades geradas para o projeto Domain e ajuste os namespaces. O scaffold gera tudo no projeto de dados, mas seguindo a arquitetura limpa, as entidades pertencem ao Domain. As configurações Fluent API ficam no projeto de dados.

Scaffold Parcial (Tabelas Específicas)

Para bancos grandes, faça scaffold apenas das tabelas necessárias:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
# Apenas tabelas do schema 'vendas'
dotnet ef dbcontext scaffold \
  "Host=localhost;Database=legado;Username=postgres;Password=senha" \
  Npgsql.EntityFrameworkCore.PostgreSQL \
  --schema vendas \
  --project src/MeuEcommerce.Infra.Data.PostgreSQL \
  --output-dir Entities/Vendas \
  --context VendasDbContext \
  --no-onconfiguring

# Ou tabelas específicas
dotnet ef dbcontext scaffold \
  "Host=localhost;Database=legado;Username=postgres;Password=senha" \
  Npgsql.EntityFrameworkCore.PostgreSQL \
  --table produtos --table categorias --table pedidos \
  --project src/MeuEcommerce.Infra.Data.PostgreSQL \
  --output-dir Entities/Core \
  --context CoreLegadoDbContext \
  --no-onconfiguring

Providers para Scaffold

O provider no comando scaffold corresponde ao banco de dados-alvo:

BancoProvider NuGet
PostgreSQLNpgsql.EntityFrameworkCore.PostgreSQL
SQL ServerMicrosoft.EntityFrameworkCore.SqlServer
OracleOracle.EntityFrameworkCore
MySQLPomelo.EntityFrameworkCore.MySql
SQLiteMicrosoft.EntityFrameworkCore.Sqlite

Gerenciando Migrations em Times com Múltiplos Desenvolvedores

Este é um dos maiores desafios práticos do EF Core em projetos corporativos. Quando 3, 5 ou 10 desenvolvedores criam migrations simultaneamente, conflitos são inevitáveis se não houver processo claro.

O Problema: Conflitos de Migration

Imagine dois desenvolvedores trabalhando em branches separadas:

1
2
3
4
5
main ─────────────── Migration_001 (InitialCreate)
  ├── feat/produtos ── Migration_002_AdicionarProdutos (Dev A)
  └── feat/pedidos ─── Migration_002_AdicionarPedidos  (Dev B)

Ambas migrations têm o mesmo snapshot como base (após Migration_001). Quando Dev B faz merge após Dev A, o EF Core detecta que o snapshot está inconsistente e falha.

Estratégia 1: Rebase da Migration (Recomendado)

Quando houver conflito, remova a migration conflitante e recrie-a sobre o estado atual:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Dev B após pull/merge da branch do Dev A:

# 1. Remover a migration conflitante (ANTES de aplicar ao banco)
dotnet ef migrations remove \
  --project src/MeuEcommerce.Infra.Data.PostgreSQL

# 2. Verificar que o banco local está atualizado com as migrations do Dev A
dotnet ef database update \
  --project src/MeuEcommerce.Infra.Data.PostgreSQL

# 3. Recriar a migration sobre o estado correto
dotnet ef migrations add AdicionarPedidos \
  --project src/MeuEcommerce.Infra.Data.PostgreSQL

Estratégia 2: Merge do Model Snapshot

O arquivo AppDbContextModelSnapshot.cs é o que causa a maioria dos conflitos de merge. Trate-o assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Em caso de conflito no snapshot, SEMPRE:
# 1. Aceite a versão da branch alvo (main/develop)
git checkout --theirs src/MeuEcommerce.Infra.Data.PostgreSQL/Migrations/AppDbContextModelSnapshot.cs

# 2. Remova sua migration
dotnet ef migrations remove --project src/MeuEcommerce.Infra.Data.PostgreSQL

# 3. Atualize o banco local
dotnet ef database update --project src/MeuEcommerce.Infra.Data.PostgreSQL

# 4. Recrie sua migration com o snapshot correto como base
dotnet ef migrations add SuaMigration --project src/MeuEcommerce.Infra.Data.PostgreSQL

Estratégia 3: Convenção de Nomes com Timestamp

Use nomes de migration com timestamp para evitar colisões:

1
2
3
4
5
6
7
8
# Em vez de nomes genéricos que podem colidir:
dotnet ef migrations add 20260118_AdicionarTabelaPedidos

# Formato recomendado: YYYYMMDD_NomeDaMigration
# Exemplos:
dotnet ef migrations add 20260118_AdicionarPedidos
dotnet ef migrations add 20260119_AlterarProdutoSku
dotnet ef migrations add 20260120_CriarIndiceProdutoNome

Regras de Ouro Para o Time

  1. Nunca edite uma migration já aplicada ao banco de produção/staging — crie uma nova migration para corrigir.

  2. Faça merge da main na sua branch ANTES de criar a migration — garante que o snapshot está atualizado.

  3. Uma feature, uma migration — evite múltiplas migrations por PR. Se precisar de ajustes, remova, ajuste a entidade e recrie.

  4. Revise migrations no Pull Request — o SQL gerado (dotnet ef migrations script) deve ser revisado como qualquer outro código.

  5. Mantenha o snapshot no Git — o AppDbContextModelSnapshot.cs DEVE ser versionado. Nunca adicione ao .gitignore.

  6. Use --idempotent para scripts de produção — gera SQL com IF NOT EXISTS, seguro para rodar múltiplas vezes:

1
2
3
dotnet ef migrations script --idempotent \
  --project src/MeuEcommerce.Infra.Data.PostgreSQL \
  --output migration-producao.sql
  1. Comunique no canal do time antes de criar migrations — uma mensagem simples como “vou criar migration para tabela X” evita trabalho duplicado.

⚠️ Atenção: Em times maiores (8+ devs), considere designar uma branch de integração onde todas as migrations são consolidadas antes de irem para main. Isso reduz conflitos de snapshot drasticamente.


Fluxo Completo: Da Entidade ao Banco

Diagrama do fluxo completo de EF Core Migrations mostrando as etapas desde a alteração na entidade, configuração Fluent API, criação da migration, até a atualização do banco de dados PostgreSQL

Para consolidar tudo, aqui está o fluxo completo em um projeto multi-camada:

 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
# 1. Criar/editar a entidade no Domain (sem referência ao EF Core)
# src/MeuEcommerce.Domain/Entities/Pedido.cs

# 2. Criar a configuração Fluent API no Infra.Data
# src/MeuEcommerce.Infra.Data.PostgreSQL/Configurations/PedidoConfiguration.cs

# 3. Adicionar o DbSet no AppDbContext
# public DbSet<Pedido> Pedidos => Set<Pedido>();

# 4. Registrar no IoC (se necessário — repository, etc.)

# 5. Garantir que está na branch atualizada
git pull origin main

# 6. Criar a migration
cd src/MeuEcommerce.Infra.Data.PostgreSQL
dotnet ef migrations add 20260118_AdicionarPedidos

# 7. Verificar o SQL que será gerado
dotnet ef migrations script --idempotent

# 8. Aplicar ao banco local
dotnet ef database update

# 9. Testar a aplicação

# 10. Commit e PR
git add .
git commit -m "feat(domain): adicionar entidade Pedido com Fluent API"

Comandos Essenciais de Referência

AçãoComando
Criar migrationdotnet ef migrations add Nome --project Infra.Data
Aplicar migrationsdotnet ef database update --project Infra.Data
Remover última migrationdotnet ef migrations remove --project Infra.Data
Listar migrationsdotnet ef migrations list --project Infra.Data
Gerar script SQLdotnet ef migrations script --idempotent --project Infra.Data
Script entre migrationsdotnet ef migrations script MigA MigB --project Infra.Data
Reverter migrationdotnet ef database update NomeMigAnterior --project Infra.Data
Scaffold banco existentedotnet ef dbcontext scaffold "connstr" Provider --project Infra.Data
Verificar pendentesdotnet ef migrations list --project Infra.Data (mostra Pending)

Dicas e Boas Práticas

  1. Sempre gere o script SQL antes de aplicar em produção — nunca use dotnet ef database update direto em produção. Gere com --idempotent, revise e aplique via DBA ou pipeline.

  2. Use IDesignTimeDbContextFactory — elimina a dependência do startup project e permite que cada desenvolvedor use seus próprios secrets.

  3. Separe migrations por provider — se sua aplicação suporta PostgreSQL e SQL Server, mantenha migrations separadas com MigrationsAssembly() apontando para projetos diferentes.

  4. Não ignore o snapshot — o AppDbContextModelSnapshot.cs é o “estado atual” do modelo para o EF Core CLI. Sem ele, o CLI não consegue calcular o diff para a próxima migration.

  5. Automatize com Makefile ou scripts — crie atalhos para os comandos mais usados. Para saber mais sobre automação com Makefile, veja Makefile: Automatizando tarefas para Python, Hugo e Docker.

  6. Revise o SQL gerado em PRs — adicione o output de dotnet ef migrations script como comentário no PR para que o time possa revisar o DDL.

  7. Use HasData() com cautela — seed data via HasData() gera migrations. Para dados grandes ou dinâmicos, prefira scripts SQL separados.

  8. Configure retry para database update — em CI/CD, o banco pode não estar pronto. Use:

1
2
3
4
5
6
7
optionsBuilder.UseNpgsql(connectionString, npgsql =>
{
    npgsql.EnableRetryOnFailure(
        maxRetryCount: 5,
        maxRetryDelay: TimeSpan.FromSeconds(30),
        errorCodesToAdd: null);
});

Conclusão

Gerenciar EF Core Migrations em projetos multi-camada exige configuração específica, mas a recompensa é uma base de código organizada, segura e sustentável. A IDesignTimeDbContextFactory resolve o problema de design-time em soluções com múltiplos csproj. O dotnet user-secrets mantém connection strings fora do código versionado. O scaffolding permite integrar bancos legados sem reescrever o schema manualmente. E com convenções claras de nomes, rebase de migrations e comunicação no time, é possível escalar para times grandes sem conflitos constantes.

O EF Core CLI é poderoso, mas exige disciplina. Estabeleça as regras do time desde o início do projeto, documente o fluxo de criação de migrations no README, e revise o SQL gerado em todo Pull Request. Para entender como o mapeamento avançado com Fluent API gera essas migrations corretamente, confira EF Core 8 Fluent API: Mapeamento, ORM e Desacoplamento.


Leia Também


Referências