efcore-patterns

Melhores práticas do Entity Framework Core, incluindo NoTracking por padrão, divisão de consultas para coleções de navegação, gerenciamento de migrações, serviços dedicados de migração e armadilhas comuns a serem evitadas.

O que é efcore-patterns?

Este skill fornece um guia abrangente de melhores práticas para usar o Entity Framework Core (EF Core) em projetos .NET, com foco em performance, manutenção e prevenção de erros comuns. Ele cobre cinco padrões principais: configuração de NoTracking por padrão para otimizar consultas de leitura, proibição de edição manual de arquivos de migração, uso de um serviço dedicado de migração (especialmente com .NET Aspire) para separar a execução das migrações da inicialização da aplicação, emprego de ExecutionStrategy para lidar com falhas transitórias de banco de dados e uso de operações em lote (ExecuteUpdate/ExecuteDelete) para atualizações e exclusões em massa. O skill também aborda armadilhas comuns como o problema N+1, conflitos de rastreamento entre instâncias do DbContext e esquecimento de marcar entidades para atualização quando o NoTracking está ativo. Ele é útil para qualquer desenvolvedor que trabalhe com EF Core em aplicações ASP.NET Core, serviços em background ou orquestrações com .NET Aspire. As práticas são apresentadas com exemplos de código C# e comandos de CLI, demonstrando como configurar o DbContext, gerenciar migrações, criar um serviço de migração e otimizar consultas. O conteúdo é baseado na documentação oficial do EF Core e em padrões de mercado.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Aaronontheweb/dotnet-skills/tree/main/skills/efcore-patterns

Installed? Explore more Programação e Desenvolvimento skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Padrões do Entity Framework Core

Quando Usar Este Skill

Use este skill quando:

  • Configurando o EF Core em um novo projeto
  • Otimizando a performance de consultas
  • Gerenciando migrações de banco de dados
  • Integrando o EF Core com .NET Aspire
  • Depurando problemas de rastreamento de alterações
  • Carregando múltiplas coleções de navegação de forma eficiente (divisão de consultas)

Princípios Fundamentais

  1. NoTracking por Padrão - A maioria das consultas é somente leitura; opte pelo rastreamento apenas quando necessário
  2. Nunca Edite Migrações Manualmente - Sempre use comandos da CLI
  3. Serviço Dedicado de Migração - Separe a execução de migrações da inicialização da aplicação
  4. ExecutionStrategy para Repetições - Lide com falhas transitórias de banco de dados
  5. Atualizações Explícitas - Com NoTracking, marque entidades explicitamente para atualização

Padrão 1: NoTracking por Padrão

Configure seu DbContext para desabilitar o rastreamento de alterações por padrão. Isso melhora a performance para cargas de trabalho com muitas leituras.

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
        // Desabilita o rastreamento de alterações por padrão para melhor performance em consultas somente leitura
        // Use .AsTracking() explicitamente para consultas que precisam rastrear alterações
        ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
    }

    public DbSet<Order> Orders => Set<Order>();
    public DbSet<Customer> Customers => Set<Customer>();
}

Quando o NoTracking Está Ativo

Consultas somente leitura funcionam normalmente:

// ✅ Leitura rápida - sem overhead de rastreamento
var orders = await dbContext.Orders
    .Where(o => o.Status == OrderStatus.Pending)
    .ToListAsync();

Gravações requerem tratamento explícito:

// ❌ ERRADO - Entidade não rastreada, SaveChanges não faz nada
var order = await dbContext.Orders.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
await dbContext.SaveChangesAsync(); // Nada acontece!

// ✅ CORRETO - Marcar entidade explicitamente para atualização
var order = await dbContext.Orders.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
dbContext.Orders.Update(order); // Marca toda a entidade como modificada
await dbContext.SaveChangesAsync();

// ✅ TAMBÉM CORRETO - Use AsTracking() para a consulta
var order = await dbContext.Orders
    .AsTracking()
    .FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
await dbContext.SaveChangesAsync(); // Funciona!

Quando Usar Rastreamento

CenárioUsar Rastreamento?Por que
Exibir dados na UINãoSomente leitura, sem atualizações
Endpoints GET da APINãoRetornando dados, sem mutações
Atualizar entidade únicaSim ou Update() explícitoPrecisa salvar alterações
Atualização complexa com navegaçãoSimRastreamento gerencia relacionamentos
Operações em loteNão + ExecuteUpdateMais eficiente

Padrão de Adição/Atualização Explícita

public class OrderService
{
    private readonly ApplicationDbContext _db;

    // CREATE - Sempre use Add (funciona independentemente do rastreamento)
    public async Task<Order> CreateOrderAsync(Order order)
    {
        _db.Orders.Add(order);
        await _db.SaveChangesAsync();
        return order;
    }

    // UPDATE - Marcar explicitamente como modificado
    public async Task UpdateOrderStatusAsync(Guid orderId, OrderStatus newStatus)
    {
        var order = await _db.Orders.FirstOrDefaultAsync(o => o.Id == orderId)
            ?? throw new NotFoundException($"Pedido {orderId} não encontrado");

        order.Status = newStatus;
        order.UpdatedAt = DateTimeOffset.UtcNow;

        // Marcar explicitamente como modificado já que DbContext usa NoTracking por padrão
        _db.Orders.Update(order);
        await _db.SaveChangesAsync();
    }

    // DELETE - Anexar e remover
    public async Task DeleteOrderAsync(Guid orderId)
    {
        var order = new Order { Id = orderId };
        _db.Orders.Remove(order);
        await _db.SaveChangesAsync();
    }
}

Padrão 2: Nunca Edite Migrações Manualmente

CRÍTICO: Sempre use comandos da CLI do EF Core para gerenciar migrações. Nunca:

  • Edite arquivos de migração manualmente (exceto para SQL personalizado em Up()/Down())
  • Exclua arquivos de migração diretamente
  • Renomeie arquivos de migração
  • Copie migrações entre projetos

Criando Migrações

# Criar uma nova migração
dotnet ef migrations add AddCustomerTable \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

# Com um DbContext específico (se você tiver vários)
dotnet ef migrations add AddCustomerTable \
    --context ApplicationDbContext \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

Removendo Migrações

# Remover a última migração (se ainda não aplicada)
dotnet ef migrations remove \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

# NUNCA faça isso:
# rm Migrations/20240101_AddCustomerTable.cs  # ❌ RUIM!

Aplicando Migrações

# Aplicar todas as migrações pendentes
dotnet ef database update \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

# Aplicar até uma migração específica
dotnet ef database update AddCustomerTable \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

# Reverter para uma migração anterior
dotnet ef database update PreviousMigrationName \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

Gerando Scripts SQL

# Gerar script SQL para todas as migrações
dotnet ef migrations script \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api \
    --output migrations.sql

# Gerar script idempotente (seguro para executar várias vezes)
dotnet ef migrations script \
    --idempotent \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

Padrão 3: Serviço Dedicado de Migração com Aspire

Separe a execução de migrações da sua aplicação principal usando um serviço de migração dedicado. Isso garante:

  • Migrações concluídas antes do aplicativo iniciar
  • Separação clara de responsabilidades
  • Semeadura controlada em ambientes de teste

Estrutura de Projeto

src/
├── MyApp.AppHost/           # Orquestração Aspire
├── MyApp.Api/               # Aplicação principal
├── MyApp.Infrastructure/    # DbContext e migrações
└── MyApp.MigrationService/  # Executor de migração dedicado

Program.cs do MigrationService

using MyApp.Infrastructure.Data;
using MyApp.MigrationService;
using Microsoft.EntityFrameworkCore;

var builder = Host.CreateApplicationBuilder(args);

// Adiciona defaults de serviço Aspire
builder.AddServiceDefaults();

// Adiciona DbContext PostgreSQL
var connectionString = builder.Configuration.GetConnectionString("appdb")
    ?? throw new InvalidOperationException("String de conexão 'appdb' não encontrada.");

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString, npgsqlOptions =>
        npgsqlOptions.MigrationsAssembly("MyApp.Infrastructure")));

// Adiciona o worker de migração
builder.Services.AddHostedService<MigrationWorker>();

var host = builder.Build();
host.Run();

MigrationWorker.cs

public class MigrationWorker : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly IHostApplicationLifetime _hostApplicationLifetime;
    private readonly ILogger<MigrationWorker> _logger;

    public MigrationWorker(
        IServiceProvider serviceProvider,
        IHostApplicationLifetime hostApplicationLifetime,
        ILogger<MigrationWorker> logger)
    {
        _serviceProvider = serviceProvider;
        _hostApplicationLifetime = hostApplicationLifetime;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Serviço de migração iniciando...");

        try
        {
            using var scope = _serviceProvider.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

            await RunMigrationsAsync(dbContext, stoppingToken);

            _logger.LogInformation("Serviço de migração concluído com sucesso.");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Serviço de migração falhou: {Error}", ex.Message);
            throw;
        }
        finally
        {
            // Interrompe a aplicação após as migrações serem concluídas
            _hostApplicationLifetime.StopApplication();
        }
    }

    private async Task RunMigrationsAsync(ApplicationDbContext dbContext, CancellationToken ct)
    {
        // Usa estratégia de execução para lidar com falhas transitórias
        var strategy = dbContext.Database.CreateExecutionStrategy();

        await strategy.ExecuteAsync(async () =>
        {
            var pendingMigrations = await dbContext.Database.GetPendingMigrationsAsync(ct);

            if (pendingMigrations.Any())
            {
                _logger.LogInformation("Aplicando {Count} migrações pendentes...",
                    pendingMigrations.Count());

                await dbContext.Database.MigrateAsync(ct);

                _logger.LogInformation("Migrações aplicadas com sucesso.");
            }
            else
            {
                _logger.LogInformation("Nenhuma migração pendente. Banco de dados está atualizado.");
            }
        });
    }
}

Configuração do AppHost

var builder = DistributedApplication.CreateBuilder(args);

var postgres = builder.AddPostgres("postgres");
var db = postgres.AddDatabase("appdb");

// Migrações executam primeiro, depois encerram
var migrations = builder.AddProject<Projects.MyApp_MigrationService>("migrations")
    .WaitFor(db)
    .WithReference(db);

// API aguarda a conclusão das migrações
var api = builder.AddProject<Projects.MyApp_Api>("api")
    .WaitForCompletion(migrations)  // Chave: aguarda as migrações terminarem
    .WithReference(db);

Padrão 4: ExecutionStrategy para Falhas Transitórias

Sempre use CreateExecutionStrategy() para operações que podem falhar transitoriamente:

public async Task UpdateWithRetryAsync(Guid id, Action<Order> update)
{
    var strategy = _dbContext.Database.CreateExecutionStrategy();

    await strategy.ExecuteAsync(async () =>
    {
        var order = await _dbContext.Orders
            .AsTracking()
            .FirstOrDefaultAsync(o => o.Id == id);

        if (order is null) return;

        update(order);
        await _dbContext.SaveChangesAsync();
    });
}

Importante: Você não pode usar CreateExecutionStrategy() com transações iniciadas pelo usuário. Se precisar de transações com retry:

var strategy = _dbContext.Database.CreateExecutionStrategy();

await strategy.ExecuteAsync(async () =>
{
    // A transação deve estar DENTRO do callback da estratégia
    await using var transaction = await _dbContext.Database.BeginTransactionAsync();

    try
    {
        // ... suas operações ...
        await _dbContext.SaveChangesAsync();
        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
});

Padrão 5: Operações em Lote com ExecuteUpdate/ExecuteDelete

Para operações em lote, use ExecuteUpdateAsync e ExecuteDeleteAsync do EF Core 7+ em vez de carregar entidades:

// ❌ LENTO - Carrega todas as entidades na memória
var expiredOrders = await _db.Orders
    .Where(o => o.ExpiresAt < DateTimeOffset.UtcNow)
    .ToListAsync();

foreach (var order in expiredOrders)
{
    order.Status = OrderStatus.Expired;
}
await _db.SaveChangesAsync();

// ✅ RÁPIDO - Única instrução SQL UPDATE
await _db.Orders
    .Where(o => o.ExpiresAt < DateTimeOffset.UtcNow)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(o => o.Status, OrderStatus.Expired)
        .SetProperty(o => o.UpdatedAt, DateTimeOffset.UtcNow));

// ✅ RÁPIDO - Única instrução SQL DELETE
await _db.Orders
    .Where(o => o.Status == OrderStatus.Cancelled && o.CreatedAt < cutoffDate)
    .ExecuteDeleteAsync();

Armadilhas Comuns

1. Esquecer de Atualizar Quando NoTracking

// ❌ Falha silenciosa - entidade não rastreada
var customer = await _db.Customers.FindAsync(id);
customer.Name = "Novo Nome";
await _db.SaveChangesAsync(); // Não faz nada!

// ✅ Atualização explícita
var customer = await _db.Customers.FindAsync(id);
customer.Name = "Novo Nome";
_db.Customers.Update(customer);
await _db.SaveChangesAsync();

2. Problema de Consulta N+1

// ❌ Consultas N+1 - uma consulta por pedido
var customers = await _db.Customers.ToListAsync();
foreach (var customer in customers)
{
    var orders = customer.Orders; // Carga lenta dispara consulta
}

// ✅ Carga antecipada - consulta única
var customers = await _db.Customers
    .Include(c => c.Orders)
    .ToListAsync();

3. Conflitos de Rastreamento com Múltiplas Instâncias de DbContext

// ❌ Conflito de rastreamento - entidade rastreada por contexto diferente
var order1 = await _db1.Orders.AsTracking().FindAsync(id);
var order2 = await _db2.Orders.AsTracking().FindAsync(id);
order2.Status = OrderStatus.Shipped;
await _db2.SaveChangesAsync(); // Pode lançar exceção ou se comportar inesperadamente

// ✅ Use contexto único ou desanexe primeiro
_db1.Entry(order1).State = EntityState.Detached;

4. Não Usar Async Consistentemente

// ❌ Chamada bloqueante em contexto async
var orders = _db.Orders.ToList(); // Bloqueia a thread

// ✅ Async em toda a cadeia
var orders = await _db.Orders.ToListAsync();

5. Consultar Dentro de Loops

// ❌ Consulta por iteração
foreach (var orderId in orderIds)
{
    var order = await _db.Orders.FindAsync(orderId);
    // processar pedido
}

// ✅ Consulta única
var orders = await _db.Orders
    .Where(o => orderIds.Contains(o.Id))
    .ToListAsync();

Tempo de Vida do DbContext na Injeção de Dependência

ASP.NET Core (Escopo por Padrão)

// Escopo = uma instância por requisição HTTP
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString));

...

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

akka-hosting-actor-patterns

Patterns for building entity actors with Akka.Hosting - GenericChildPerEntityParent, message extractors, cluster sharding abstraction, akka-reminders, and ITimeProvider. Supports both local testing and clustered production modes.

akka-net-aspire-configuration

Configure Akka.NET with .NET Aspire for local development and production deployments. Covers actor system setup, clustering, persistence, Akka.Management integration, and Aspire orchestration patterns.

akka-net-best-practices

Critical Akka.NET best practices including EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution patterns, and cluster/local mode abstractions for testability.

akka-net-management

Akka.Management for cluster bootstrapping, service discovery (Kubernetes, Azure, Config), health checks, and dynamic cluster formation without static seed nodes.

akka-net-testing-patterns

Write unit and integration tests for Akka.NET actors using modern Akka.Hosting.TestKit patterns. Covers dependency injection, TestProbes, persistence testing, and actor interaction verification. Includes guidance on when to use traditional TestKit.

api-design

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

aspire-configuration

Configure Aspire AppHost to emit explicit app config via environment variables; keep app code free of Aspire clients and service discovery.

aspire-integration-testing

Write integration tests using .NET Aspire

aspire-service-defaults

Create a shared ServiceDefaults project for Aspire applications. Centralizes OpenTelemetry, health checks, resilience, and service discovery configuration across all services.

crap-analysis

Analyze code coverage and CRAP (Change Risk Anti-Patterns) scores to identify high-risk code. Use OpenCover format with ReportGenerator for Risk Hotspots showing cyclomatic complexity and untested code paths.

csharp-concurrency-patterns

Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and manual synchronization unless absolutely necessary.

database-performance

Database access patterns for performance. Separate read/write models, avoid N+1 queries, use AsNoTracking, apply row limits, and never do application-side joins. Works with EF Core and Dapper.

dependency-injection-patterns

Organize DI registrations using IServiceCollection extension methods. Group related services into composable Add* methods for clean Program.cs and reusable configuration in tests.

dotnet-devcert-trust

Diagnose and fix .NET HTTPS dev certificate trust issues on Linux. Covers the full certificate lifecycle from generation to system CA bundle inclusion, with distro-specific guidance for Ubuntu, Fedora, Arch, and WSL2.

dotnet-local-tools

Managing local .NET tools with dotnet-tools.json for consistent tooling across development environments and CI/CD pipelines.

dotnet-project-structure

Modern .NET project structure including .slnx solution format, Directory.Build.props, central package management, SourceLink, version management with RELEASE_NOTES.md, and SDK pinning with global.json.

dotnet-slopwatch

Use Slopwatch to detect LLM reward hacking in .NET code changes. Run after every code modification to catch disabled tests, suppressed warnings, empty catch blocks, and other shortcuts that mask real problems.

ilspy-decompile

Understand implementation details of .NET code by decompiling assemblies. Use when you want to see how a .NET API works internally, inspect NuGet package source, view framework implementation, or understand compiled .NET binaries.

mailpit-integration

Test email sending locally using Mailpit with .NET Aspire. Captures all outgoing emails without sending them. View rendered HTML, inspect headers, and verify delivery in integration tests.

marketplace-publishing

Workflow for publishing skills and agents to the dotnet-skills Claude Code marketplace. Covers adding new content, updating plugin.json, validation, and release tagging.

Habilidades Relacionadas