EF
Core : Ciclo de vida do DbContext em APIs
![]() |
Hoje vamos tratar sobre o ciclo de vida do DbContext em aplicações web focando nas APIs. |
Compreender o ciclo de vida do DbContext em aplicações web é crucial devido à sua natureza desconectada. Ao contrário de aplicações conectadas (como as de desktop onde o acesso aos dados é contínuo), em aplicações web, a interface do usuário fica separada da lógica de backend.
![]() |
Em aplicações web, cada requisição (para buscar ou atualizar dados) cria uma nova instância do DbContext. Assim que a operação termina, essa instância é descartada. Isso ocorre porque o servidor não pode compartilhar uma única instância do DbContext entre todos os usuários da web, e não há sentido em manter o DbContext em memória quando a interação do usuário acontece no navegador.
A natureza desconectada das aplicações web torna o cenário perfeito para consultas no-tracking. Como o DbContext não precisa rastrear as alterações em objetos que são enviados pela internet (já que o servidor não tem como saber o que acontece no navegador), desabilitar o rastreamento por padrão melhora o desempenho da consulta, pois o DbContext não perde tempo construindo objetos de rastreamento.
Ao enviar objetos de volta para a aplicação web para persistência no banco de dados, a nova instância do DbContext não terá conhecimento do histórico do objeto.
Isso significa que
é sua responsabilidade informar ao EF Core se um objeto precisa ser
inserido, atualizado ou excluído do banco de dados. Isso pode ser feito
usando os métodos Add,
Update e
Remove
do DbSet,
definindo explicitamente o estado da entrada através do contexto, ou aplicando
os valores recebidos diretamente aos valores atuais do banco de dados.
Com isso podemos resumir os principais conceitos para os quais o desenvolvedor deve atentar:
✅ DbContext por Requisição: Em aplicações web, cada requisição deve ter sua própria instância de DbContext.
✅ No-Tracking Queries: Consultas sem rastreamento melhoram desempenho em cenários somente leitura.
✅ Gerenciamento Explícito de Estados: O desenvolvedor deve definir se um objeto é novo, modificado ou excluído.
✅ Padrão Desconectado: Comum em APIs RESTful e aplicações web, onde o cliente e o servidor não mantêm estado contínuo.
Parte prática : Criando uma API com EF Core no cenário desconectado
Vamos criar um
exemplo de Web API ASP.NET Core com EF Core para ilustrar os conceitos de
gerenciamento de DbContext e
consultas "no-tracking" em um CRUD de produtos
Objetivo:
Ter uma API que permita:
Criar um produto.
Ler (listar todos ou obter por ID) produtos.
Atualizar um
produto.
Deletar um produto.
Pré-requisitos:
.NET SDK instalado.
SQL Server (Express, LocalDB ou qualquer
instância acessível).
Visual Studio Code ou Visual Studio.
Vamos começar
criando um novo projeto ASP.NET Core Web API.
Abra seu terminal ou prompt
de comando e execute:
dotnet new webapi
-n ProdutoApiExemplo
cd
ProdutoApiExemplo
Instale os Pacotes NuGet do EF Core.
Precisamos dos pacotes para o EF Core e o SQL Server.
dotnet add package
Microsoft.EntityFrameworkCore
dotnet add package
Microsoft.EntityFrameworkCore.SqlServer
dotnet add package
Microsoft.EntityFrameworkCore.Design # Necessário para
migrações
Crie uma pasta Models e adicione o arquivo Produto.cs:
namespace ProdutoApiExemplo.Models { public class Produto { public int Id { get; set; } public string Nome { get; set; } public decimal Preco { get; set; } public int Estoque { get; set; } } } |
Crie uma pasta Data e adicione o arquivo ApplicationDbContext.cs. Este será nosso DbContext que interage com o banco de dados.
using Microsoft.EntityFrameworkCore; using ProdutoApiExemplo.Models; namespace ProdutoApiExemplo.Data { public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Produto> Produtos { get; set; }
// Opcional: Configurar o DbContext para ser no-tracking por padrão protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { // Isso define o comportamento padrão de rastreamento de consultas para "NoTracking" // Assim, todas as consultas feitas por esta // instância do DbContext não rastrearão entidades por padrão. optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); } // Opcional: Adicionar algumas sementes de dados (seed data) protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Produto>().HasData( new Produto { Id = 1, Nome = "Laptop", Preco = 1500.00m, Estoque = 10 }, new Produto { Id = 2, Nome = "Mouse", Preco = 25.00m, Estoque = 50 }, new Produto { Id = 3, Nome = "Teclado", Preco = 75.00m, Estoque = 30 } ); } } } |
Neste código : optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
Configuramos o DbContext para que todas as consultas sejam no-tracking por padrão, como mencionado no texto. Isso significa que, a menos que você explicitamente chame .AsTracking(), as entidades retornadas pelas consultas não serão rastreadas pelo Change Tracker, otimizando o desempenho para cenários web.
Abra o arquivo Program.cs e configure a injeção de dependência do DbContext e a string de conexão.
using Microsoft.EntityFrameworkCore; using ProdutoApiExemplo.Data; var builder = WebApplication.CreateBuilder(args); // Adicionar serviços ao contêiner DI builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Configurar a string de conexão var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); // Aqui, o DbContext é configurado para ser 'Scoped'. // Isso significa que uma nova instância do DbContext será criada para cada requisição HTTP // e descartada quando a requisição for concluída. // Este é o padrão recomendado para aplicações web. builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString)); var app = builder.Build(); // Configurar o pipeline de requisição HTTP. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); // Aplicar migrações automaticamente no início da aplicação using (var scope = app.Services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>(); dbContext.Database.Migrate(); } app.Run(); |
Em aplicações web, um serviço Scoped significa que uma nova instância é criada para cada requisição HTTP. Isso se alinha perfeitamente com o conceito de que "cada método para recuperar ou atualizar dados instanciará sua própria instância DbContext e, em seguida, a descartará quando terminar."
Cada vez que um cliente faz uma chamada para a API, uma nova instância do ApplicationDbContext é fornecida ao controlador, e ao final da requisição, ela é automaticamente descartada pelo sistema de injeção de dependência do ASP.NET Core.
Agora no arquivo appsettings.json defina a string de conexão com o banco de dados:
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ProdutoApiDb; Trusted_Connection=True;MultipleActiveResultSets=true" // Ou para SQL Server completo: "Server=SEU_SERVIDOR;Database=ProdutoApiDb;User Id=SEU_USUARIO;Password=SUA_SENHA;" } } |
Abra o
terminal na raiz do seu projeto (ProdutoApiExemplo)
e execute:
dotnet ef migrations
add InitialCreate
dotnet
ef database update
Crie uma pasta Controllers e adicione o arquivo ProdutosController.cs.
using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using ProdutoApiExemplo.Data; using ProdutoApiExemplo.Models; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace ProdutoApiExemplo.Controllers { [Route("api/[controller]")] [ApiController] public class ProdutosController : ControllerBase { private readonly ApplicationDbContext _context; public ProdutosController(ApplicationDbContext context) { _context = context; } // GET: api/Produtos [HttpGet] public async Task<ActionResult<IEnumerable<Produto>>> GetProdutos() { // Devido à configuração no ApplicationDbContext, esta consulta já // é no-tracking por padrão. // Isso é ideal para leitura de dados em uma API, pois não precisamos // rastrear alterações. return await _context.Produtos.ToListAsync(); } // GET: api/Produtos/5 [HttpGet("{id}")] public async Task<ActionResult<Produto>> GetProduto(int id) { // Esta consulta também é no-tracking por padrão. var produto = await _context.Produtos.FindAsync(id); if (produto == null) { return NotFound(); } return produto; } // PUT: api/Produtos/5 // Para atualizar um produto em uma aplicação desconectada: // 1. O cliente envia o produto completo (com o Id). // 2. O servidor precisa informar ao EF Core que este objeto deve ser atualizado. [HttpPut("{id}")] public async Task<IActionResult> PutProduto(int id, Produto produto) { if (id != produto.Id) { return BadRequest("O ID na URL não corresponde ao ID do produto no corpo da requisição."); } // A nova instância do DbContext não tem histórico deste 'produto'. // Precisamos informar explicitamente ao EF Core que o estado deste objeto é 'Modified'. _context.Entry(produto).State = EntityState.Modified; try { await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { if (!ProdutoExists(id)) { return NotFound(); } else { throw; } } return NoContent(); } // POST: api/Produtos // Para criar um produto: // O cliente envia um novo produto (sem Id ou com Id=0). // O servidor adiciona-o ao DbSet, e o EF Core o marca como 'Added'. [HttpPost] public async Task<ActionResult<Produto>> PostProduto(Produto produto) { // O novo DbContext não tem ideia da história deste objeto. // Ao adicioná-lo ao DbSet, o EF Core o marca como 'Added'. _context.Produtos.Add(produto); await _context.SaveChangesAsync(); // Retorna o produto criado, incluindo o Id gerado pelo banco de dados. return CreatedAtAction("GetProduto", new { id = produto.Id }, produto); } // DELETE: api/Produtos/5 // Para deletar um produto: // O cliente envia o Id do produto a ser deletado. // O servidor recupera o produto (ou cria uma instância stub) e o marca para remoção. [HttpDelete("{id}")] public async Task<IActionResult> DeleteProduto(int id) { // Esta é uma maneira de lidar com exclusões em cenários desconectados: // Primeiro, tenta encontrar o produto. // FindAsync pode usar AsTracking() internamente, mas para exclusão não importa. var produto = await _context.Produtos.FindAsync(id); if (produto == null) { return NotFound(); } // Uma vez que temos a entidade (seja ela rastreada ou não no momento), // a marcamos para remoção. _context.Produtos.Remove(produto); await _context.SaveChangesAsync(); return NoContent(); } private bool ProdutoExists(int id) { // Usamos .AsNoTracking() explicitamente aqui para garantir que esta verificação // não adicione o produto ao Change Tracker se ele existir, // pois não precisamos rastreá-lo apenas para verificar sua existência. return _context.Produtos.Any(e => e.Id == id); } } } |
Entendendo o código:
Injeção de Dependência: O ApplicationDbContext é injetado no construtor do ProdutosController. Isso significa que cada vez que uma requisição atinge este controller, uma nova instância do ApplicationDbContext é fornecida.
Consultas "No-Tracking" por Padrão: Nos métodos GetProdutos e GetProduto, como configuramos UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking) no DbContext, as entidades retornadas por _context.Produtos.ToListAsync() e _context.Produtos.FindAsync(id) não serão rastreadas automaticamente.
Isso é o ideal para operações de leitura em APIs, onde você não precisa que o EF Core monitore essas entidades para futuras alterações na mesma requisição.
Gerenciamento do Estado para Atualização (HttpPut):
No método PutProduto, o objeto produto que chega do cliente é uma nova instância.
A linha _context.Entry(produto).State = EntityState.Modified; é crucial.
Ela informa explicitamente ao EF Core que este objeto, apesar de não ter sido originalmente carregado por esta instância do DbContext, deve ser tratado como uma entidade existente que teve seus valores modificados e precisa ser persistida de volta ao banco de dados. Isso resolve o problema de "a nova instância do DbContext não terá nenhuma pista sobre o histórico desse objeto".
Gerenciamento do Estado para Criação (HttpPost): No método PostProduto, _context.Produtos.Add(produto); é suficiente. Quando você adiciona um novo objeto a um DbSet em um DbContext fresco, o EF Core automaticamente infere que ele é uma nova entidade e a marca como Added.
Gerenciamento do Estado para Exclusão (HttpDelete): No DeleteProduto, primeiro tentamos encontrar o produto usando FindAsync. Uma vez que a entidade é recuperada (e se existir), _context.Produtos.Remove(produto); a marca como Deleted. Se o produto não for encontrado, retornamos NotFound.
Testando os endpoints da API
Abra o terminal na
raiz do projeto e execute: dotnet run
A aplicação será iniciada, e você verá o URL onde o Swagger UI está
disponível (geralmente
https://localhost:XXXX/swagger).
Abra seu navegador
e navegue até o URL do Swagger UI. Você verá os endpoints CRUD para Produtos:
GET /api/Produtos: Listar todos os produtos.
Comportamento: A consulta é "no-tracking" por padrão, sem
sobrecarga de rastreamento.
GET /api/Produtos/{id}:
Obter um produto por ID.
Comportamento: Também
"no-tracking" por padrão.
POST /api/Produtos: Criar
um novo produto.
| { "nome": "Monitor", "preco": 250.00, "estoque": 15 } |
Comportamento: O EF Core identifica o objeto como novo e o insere.
PUT
/api/Produtos/{id}: Atualizar um produto existente.
Corpo da
Requisição (Exemplo - para atualizar o produto com Id 1):
{ "id": 1, "nome": "Laptop Premium", "preco": 1800.00, "estoque": 8 } |
Comportamento: O
_context.Entry(produto).State = EntityState.Modified;
informa ao EF Core para atualizar a entidade com base no ID fornecido.
DELETE
/api/Produtos/{id}: Deletar um produto por ID.
Comportamento: O produto é recuperado (ou uma instância é criada e
marcada como excluída) e então removido do banco de dados.
Conclusão
Este exemplo
demonstra claramente os princípios de gerenciamento do DbContext
em uma aplicação web API:
Instâncias Curtas do DbContext:
A injeção de dependência Scoped garante que um DbContext novo
seja criado para cada requisição HTTP e descartado em seguida.
Consultas "No-Tracking": A configuração padrão no ApplicationDbContext otimiza as operações de leitura.
Gerenciamento Explícito do Estado: Para operações de escrita (Atualizar, Deletar), o desenvolvedor assume a responsabilidade de informar ao EF Core o estado da entidade, usando:
_context.Entry(entity).State = EntityState.Modified; ou _context.Produtos.Remove(entity);.
E estamos
conversados...
![]()
"Ninguém pode servir a dois senhores; porque ou há de odiar um e amar o
outro, ou se dedicará a um e desprezará o outro. Não podeis servir a Deus e a
Mamom."
Mateus 6:24a
Referências:
NET - Unit of Work - Padrão Unidade de ...