EF Core - Configurando o DbContext


 Hoje veremos como configurar o DbContext em tempo de projeto em aplicações ASP .NET Core.

As ferramentas em tempo de projeto do EF Core, como Migrations, precisam ser capazes de descobrir e criar uma instância de trabalho do tipo DbContext para reunir detalhes sobre os tipos de entidade do aplicativo e como eles são mapeados para um esquema de banco de dados.

Este processo pode ser automático, desde que a ferramenta possa criar facilmente o DbContext de forma que ele seja configurado de forma semelhante a como seria configurado em tempo de execução.

Embora qualquer padrão que forneça as informações de configuração necessárias para o DbContext possa funcionar em tempo de execução, as ferramentas que exigem o uso de um DbContext em tempo de projeto só podem funcionar com um número limitado de padrões.

Configurando DbContextOptions

O DbContext deve ter uma instância de DbContextOptions para realizar qualquer trabalho. A instância DbContextOptions carrega informações de configuração como:

O exemplo a seguir configura o DbContextOptions para usar o provedor do SQL Server, uma conexão contida na variável connectionString, um tempo limite de comando no nível do provedor e um seletor de comportamento EF Core que torna todas as consultas executadas no DbContext sem rastreamento por padrão:


    optionsBuilder
    .UseSqlServer(connectionString, providerOptions=>providerOptions.CommandTimeout(60))
    .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking);
 

O DbContextOptions pode ser fornecido ao DbContext substituindo o método OnConfiguring ou externamente por meio de um argumento do construtor.

Se ambos forem usados, o método OnConfiguring será aplicado por último e pode sobrescrever as opções fornecidas para o argumento do construtor.

Usando um construtor com argumento

Exemplo usando o construtor da classe de contexto com argumento aceitando um DbContextOptions:

public class DemoContext : DbContext
{
    public DemoContext(DbContextOptions<DemoContext> options) : base(options)
    { }
    public DbSet<Demo> Demos { get; set; }
}

Seu aplicativo agora pode passar as DbContextOptions ao instanciar um contexto, da seguinte maneira:


var optionsBuilder = new DbContextOptionsBuilder<DemoContext>();

optionsBuilder.UseSqlite("Data Source=blog.db");
using (var context = new DemoContext(optionsBuilder.Options))
{
  //seu código
}

Usando o método OnConfiguring da classe de contexto

Você também pode inicializar DbContextOptions dentro do próprio contexto. Embora você possa usar essa técnica para configuração básica, normalmente ainda precisará obter certos detalhes de configuração de fora, por exemplo, uma string de conexão de banco de dados.

Isso pode ser feito com uma API Configuration ou qualquer outro meio.

Para inicializar DbContextOptions dentro do contexto, substitua o método OnConfiguring e chame os métodos no DbContextOptionsBuilder fornecido:

public class DemoContext : DbContext
{

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite("Data Source=blog.db");
    }
    public DbSet<Demo> Demos { get; set; }
}

Com base nesta abordagem uma aplicação pode simplesmente instanciar tal contexto sem passar nada para seu construtor:

using (var context = new DemoContext())
{
  // seu código
}

Outro exemplo de implementação do método OnConfiguring() onde definimos a obtenção da string de conexão a partir do arquivo appsettings.json :

        protected override void OnConfiguring(DbContextOptionsBuilder options)
        {
            if (!options.IsConfigured)
            {
                IConfigurationRoot configuration = new ConfigurationBuilder()
                   .SetBasePath(Directory.GetCurrentDirectory())
                   .AddJsonFile("appsettings.json")
                   .Build();
                var connectionString = configuration.GetConnectionString("DemoConnection");
                options.UseSqlServer(connectionString);
            }
        }

Usando o DbContext com injeção de dependência

Esse talvez seja a abordagem mais usada.

Como o EF Core suporta o uso de DbContext com um contêiner de injeção de dependência. Seu tipo DbContext pode ser adicionado ao contêiner de serviço usando o método AddDbContext<TContext>.

Com isso, o AddDbContext<TContext> tornará o seu tipo DbContext, o TContext e o DbContextOptions<TContext> correspondente, disponíveis para injeção a partir do contêiner de serviço.

1- Adicionando o DbContext à injeção de dependência:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<DemoContext>(options => options.UseSqlite("Data Source=blog.db"));
}
Para que isso funcione temos que adicionar um argumento de construtor ao tipo DbContext que aceita DbContextOptions<TContext>:

public class DemoContext : DbContext
{
    public DemoContext(DbContextOptions<DemoContext> options) : base(options)
    { }
    public DbSet<Demo> Demos { get; set; }
}
Com isso podemos injetar a instância do contexto no construtor do controlador da aplicação ASP .NET Core:

public class HomeController
{
    private readonly DemoContext _context;
    public HomeController(DemoContext context)
    {
      _context = context;
    }
    ...
}
Evitando problemas com o DbContext assíncrono

O Entity Framework Core não oferece suporte a várias operações paralelas em execução na mesma instância DbContext.

Isso inclui a execução paralela de consultas assíncronas e qualquer uso simultâneo explícito de várias threads. Portanto, sempre aguarde chamadas assíncronas (usando await) 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ê terá um erro InvalidOperationException com uma mensagem como esta:

"Uma segunda operação foi iniciada neste contexto antes da conclusão de uma operação anterior. Isso geralmente é causado por threads diferentes usando a mesma instância de DbContext, no entanto, os membros da instância não têm garantia de thread-safe."

Quando o acesso simultâneo não é detectado, isso pode resultar em comportamento indefinido, travamentos de aplicativos e corrupção de dados.

Existem erros comuns que podem causar inadvertidamente acesso simultâneo na mesma instância DbContext como:

1- Esquecer de esperar a conclusão de uma operação assíncrona antes de iniciar qualquer outra operação no mesmo DbContext

Os métodos assíncronos permitem que o EF Core inicie operações que acessam o banco de dados de forma não bloqueada. Mas se um chamador não esperar a conclusão de um desses métodos e continuar a realizar outras operações no DbContext, o estado do DbContext pode ser (e muito provavelmente será) corrompido.

Assim, sempre utilize a palavra-chave await nos métodos assíncronos do EF Core.

2- Compartilhamento implícito de instâncias DbContext em várias threads por meio de injeção de dependência

O método de extensão AddDbContext registra tipos DbContext com um tempo de vida com Scoped por padrão.

Este modo é protegido contra problemas de acesso simultâneo na maioria dos aplicativos ASP.NET Core porque há apenas um thread executando cada solicitação do cliente em um determinado momento e porque cada solicitação obtém um escopo de injeção de dependência separado (e, portanto, uma instância DbContext separada).

Para o modelo de hospedagem do Blazor Server, uma solicitação lógica é usada para manter o circuito do usuário do Blazor e, portanto, apenas uma instância DbContext com escopo está disponível por circuito do usuário se o modo Scoped for usado.

Qualquer código que execute explicitamente vários threads em paralelo deve garantir que as instâncias DbContext nunca sejam acessadas simultaneamente.

Usando a injeção de dependência, isso pode ser obtido registrando o contexto como Scoped e criando escopos (usando IServiceScopeFactory) para cada thread ou registrando o DbContext como temporário (usando a sobrecarga de AddDbContext que leva um parâmetro ServiceLifetime).

Por isso ler a documentação é importante para entender o comportamento básico do contexto e evitar dores de cabeça e depois não saber porque o problema esta ocorrendo.

E estamos conversados...

(Disse Jesus)"Eu sou a videira verdadeira, e meu Pai é o lavrador. Toda a vara em mim, que não dá fruto, a tira; e limpa toda aquela que dá fruto, para que dê mais fruto."
João 15:1,2

Referências:


José Carlos Macoratti