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.

O DbContext é uma classe fundamental no Entity Framework Core, que é responsável por gerenciar o acesso e as interações com o banco de dados. Ele representa a sessão atual com o banco de dados e é responsável pelo rastreamento de entidades, bem como pela execução de consultas e operações de gravação no banco de dados.



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 :

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:


José Carlos Macoratti