EF Core - Tempo de vida do DbContext , configuração e inicialização
Hoje vamos tratar da inicialização e configuração da instância do DbContext. |
Ao
criar uma instância do DbContext, o Entity Framework Core configura e inicializa
vários serviços que são necessários para se trabalhar com o banco de dados.
Esses serviços incluem um provedor de banco de dados, um rastreador de
alterações (change tracker), um gerenciador de
conexão (connection manager) e outros componentes.
O DbContext é responsável por gerenciar o ciclo de vida desses serviços.
O DbContext é criado a partir de uma classe que herda da classe abstrata DbContext e deve ser configurado para se comunicar com o banco de dados. Isso envolve a especificação do provedor de banco de dados, a conexão com o banco de dados e a configuração do mapeamento de entidades para tabelas de banco de dados. Essas configurações são definidas no construtor da classe que herda do DbContext ou no método OnConfiguring da mesma classe. (Outra opção é definir as opções usando DbContextOptions)
O tempo de vida de um DbContext começa quando a instância é criada e termina quando a instância é descartada. Uma instância DbContext é projetada para ser usada para uma única unidade de trabalho. Isso significa que o tempo de vida de uma instância DbContext geralmente é muito curto.
Uma
unidade de trabalho típica ao usar o Entity Framework Core (EF Core) envolve:
- A criação de uma instância DbContext;
- O rastreamento de instâncias de entidade pelo contexto. Entidades tornam-se
rastreadas :
- pelo que é retornado de uma
consulta;
- pelo que é adicionado ou
anexado ao contexto;
- As alterações são feitas nas entidades rastreadas conforme necessário
para implementar a regra de negócios
- As chamadas aos métodos SaveChanges ou SaveChangesAsync. O EF Core
detecta as alterações feitas e as grava no banco de dados;
- O descarte da instância do DbContext;
Aqui é importante destacar que :
É muito importante descartar o DbContext após o uso. Isso garante que todos os recursos não gerenciados sejam liberados e que todos os eventos ou outros ganchos sejam cancelados para evitar vazamentos de memória caso a instância permaneça referenciada.
O DbContext não é thread-safe. Não compartilhe contextos entre threads. Certifique-se de aguardar todas as chamadas assíncronas antes de continuar a usar a instância de contexto.
Uma exception do tipo InvalidOperationException lançada pelo código EF Core pode colocar o contexto em um estado irrecuperável. Tais exceções indicam um erro de programa e não foram projetadas para serem recuperadas.
O DbContext na injeção de dependência
Em muitos aplicativos Web, cada solicitação HTTP corresponde a uma única unidade
de trabalho. Isso faz com que vincular o tempo de vida do contexto ao da
solicitação seja um bom padrão para aplicativos da web.
Os aplicativos ASP.NET Core são configurados usando injeção de dependência, e, o
EF Core pode ser adicionado a essa configuração usando
AddDbContext na classe Program ou no método
ConfigureServices de Startup.cs.
Por exemplo:
... builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer("name=ConnectionStrings:DefaultConnection")); ... |
Este exemplo registra uma subclasse DbContext chamada ApplicationDbContext como um serviço com escopo no provedor de serviços de aplicativo ASP.NET Core (também conhecido como contêiner de injeção de dependência).
O contexto é
configurado para usar o provedor de banco de dados do SQL Server e vai ler a
string de conexão da configuração do ASP.NET Core.
A classe ApplicationDbContext deve expor um
construtor público com um parâmetro
DbContextOptions<ApplicationDbContext>. É assim que a configuração de
contexto de AddDbContext é passada para o
DbContext. Por exemplo:
public
class
ApplicationDbContext :
DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) {} } |
Com isso ApplicationDbContext pode então ser usado em controladores ASP.NET Core ou outros serviços por meio de injeção de construtor :
public
class
MeuController { private readonly ApplicationDbContext _context; public MeuController(ApplicationDbContext context) { _context = context; } } |
O resultado final é uma instância ApplicationDbContext criada para cada solicitação e passada para o controlador para executar uma unidade de trabalho antes de ser descartada quando a solicitação terminar.
Uma outra forma de construir as instâncias de DbContext é a maneira normal do .NET, por exemplo, com new em C#. A configuração pode ser executada substituindo o método OnConfiguring ou passando opções para o construtor:
public
class
ApplicationDbContext :
DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Teste"); } } |
Esse padrão também facilita a passagem de configurações como a string de conexão por meio do construtor DbContext :
public
class
ApplicationDbContext :
DbContext { private readonly string _connectionString; public ApplicationDbContext(string connectionString) { _connectionString = connectionString; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(_connectionString); } } |
Como alternativa, DbContextOptionsBuilder pode ser usado para criar um objeto DbContextOptions que é passado para o construtor DbContext. Isso permite que um DbContext configurado para injeção de dependência também seja construído explicitamente.
Por exemplo, ao usar ApplicationDbContext definido para aplicativos Web ASP.NET Core acima:
public
class
ApplicationDbContext :
DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) {} } |
O DbContextOptions pode ser criado e o construtor pode ser chamado explicitamente:
var
contextOptions =
new
DbContextOptionsBuilder<ApplicationDbContext>() .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test") .Options; using var context = new ApplicationDbContext(contextOptions); |
DbContextOptions
O ponto de partida para toda a configuração do DbContext é
DbContextOptionsBuilder. Existem três maneiras de obter este construtor:
1- Em AddDbContext e métodos relacionados
2- Em OnConfiguring
3- Construído explicitamente com novos
Exemplos de cada um deles foram mostrados acima. A mesma configuração pode ser
aplicada independentemente da origem do construtor. Além disso,
OnConfiguring é sempre chamado independentemente de
como o contexto é construído.
Isso significa que OnConfiguring pode ser usado para executar configurações adicionais mesmo quando AddDbContext estiver sendo usado.
Configurando o provedor de banco de dados
Cada instância DbContext deve ser configurada para usar apenas um provedor de
banco de dados.
Diferentes instâncias de um subtipo DbContext podem ser usadas com diferentes provedores de banco de dados, mas uma única instância deve usar apenas uma.
Um provedor de banco de dados é configurado usando uma chamada Use* específica. Por exemplo, para usar o provedor de banco de dados SQL Server:
public
class
ApplicationDbContext :
DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Teste"); } } |
Esses métodos Use* são métodos de extensão implementados pelo provedor de banco de dados. Isso significa que o pacote NuGet do provedor de banco de dados deve ser instalado antes que o método de extensão possa ser usado.
A tabela abaixo contém exemplo para os provedores de banco de dados mais comuns:
Database system | Example configuration | NuGet package |
---|---|---|
SQL Server or Azure SQL | .UseSqlServer(connectionString) | Microsoft.EntityFrameworkCore.SqlServer |
Azure Cosmos DB | .UseCosmos(connectionString, databaseName) | Microsoft.EntityFrameworkCore.Cosmos |
SQLite | .UseSqlite(connectionString) | Microsoft.EntityFrameworkCore.Sqlite |
EF Core in-memory database | .UseInMemoryDatabase(databaseName) | Microsoft.EntityFrameworkCore.InMemory |
PostgreSQL* | .UseNpgsql(connectionString) | Npgsql.EntityFrameworkCore.PostgreSQL |
MySQL/MariaDB* | .UseMySql(connectionString) | Pomelo.EntityFrameworkCore.MySql |
Oracle* | .UseOracle(connectionString) | Oracle.EntityFrameworkCore |
A configuração opcional específica para o provedor de banco de dados é executada em um construtor adicional específico do provedor.
Por exemplo, usando EnableRetryOnFailure para configurar novas tentativas para resiliência de conexão ao conectar-se ao Azure SQL:
public
class
ApplicationDbContext :
DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder .UseSqlServer( @"Server=(localdb)\mssqllocaldb;Database=Teste", providerOptions => { providerOptions.EnableRetryOnFailure(); }); } } |
Outra configuração DbContext pode ser encadeada antes ou depois (não faz diferença qual) a chamada Use*. Por exemplo, para ativar o registro de dados confidenciais:
public
class
ApplicationDbContext :
DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder .EnableSensitiveDataLogging() .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Teste"); } } |
Evitando problemas de encadeamento de DbContext
O Entity Framework Core não oferece suporte a várias operações paralelas sendo
executadas na mesma instância DbContext. Isso inclui a execução paralela de
consultas assíncronas e qualquer uso simultâneo explícito de vários threads.
Portanto, sempre aguarde chamadas assíncronas imediatamente ou use
instâncias DbContext separadas para operações executadas em paralelo.
Quando o EF Core detecta uma tentativa de usar uma instância DbContext
simultaneamente, você verá uma InvalidOperationException
com uma mensagem como esta:
Uma segunda operação foi iniciada neste contexto antes que uma operação
anterior fosse concluída. Isso geralmente é causado por threads diferentes
usando a mesma instância de DbContext, no entanto, não há garantia de que os
membros da instância sejam thread-safe.
Quando o acesso simultâneo não é detectado, pode resultar em comportamento
indefinido, falhas de aplicativos e corrupção de dados.
Existem erros comuns que podem inadvertidamente causar acesso simultâneo na
mesma instância DbContext:
-
Operações
assíncronas com falhas (Sempre aguarde os métodos assíncronos do EF Core)
- Compartilhamento implícito de instâncias DbContext via injeção de dependência
(use o tempo de vida Scoped)
E estamos conversados ...
"A estultícia do homem perverterá o seu caminho, e o seu coração se irará contra
o Senhor."
Provérbios
19:3
Referências: