Tempo de vida, configuração e inicialização do DbContext

Este artigo mostra padrões básicos de inicialização e configuração de uma instância DbContext.

O tempo de vida do DbContext

O tempo de vida de um DbContext começa quando a instância é criada e termina quando ela é descartada. Uma instância DbContext foi projetada para ser usada para uma únicaunidade de trabalho. Isso significa que o tempo de vida de uma instância DbContext geralmente é muito curto.

Dica

Para citar Martin Fowler do link acima: "Uma Unidade de Trabalho controla tudo o que você faz durante uma transação de negócios que pode afetar o banco de dados. Quando você terminar, ela identificará tudo o que precisa ser feito para alterar o banco de dados como resultado do seu trabalho."

Uma típica unidade de trabalho ao usar o EF Core (Entity Framework Core) envolve:

  • A criação de uma instância DbContext
  • O acompanhamento de instâncias de entidade pelo contexto. As entidades são controladas ao
  • São feitas alterações nas entidades controladas conforme necessário para implementar a regra de negócios
  • SaveChanges ou SaveChangesAsync é chamada. O EF Core detecta as alterações feitas e as grava no banco de dados.
  • A instância DbContext é descartada

Importante

  • É muito importante descartar o DbContext após o uso. Assim, todos os recursos não gerenciados são liberados, e quaisquer eventos ou outros ganchos não são registrados para evitar vazamentos de memória caso a instância permaneça referenciada.
  • DbContext não é thread-safe. Não compartilhe contextos entre threads. Lembre-se de aguardar todas as chamadas assíncronas antes de continuar a usar a instância de contexto.
  • Um InvalidOperationException gerado pelo código do EF Core pode colocar o contexto em um estado irrecuperável. Essas exceções indicam um erro de programa e não foram projetadas para serem recuperadas.

O DbContext na injeção de dependência para ASP.NET Core

Em muitos aplicativos Web, cada solicitação HTTP corresponde a uma única unidade de trabalho. Com isso, a associação do tempo de vida do contexto ao da solicitação se torna um bom padrão para aplicativos Web.

Aplicativos ASP.NET Core são configurados usando a injeção de dependência. O EF Core pode ser adicionado a essa configuração usando AddDbContext no método ConfigureServices de Startup.cs. Por exemplo:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();

    services.AddDbContext<ApplicationDbContext>(
        options => options.UseSqlServer("name=ConnectionStrings:DefaultConnection"));
}

Esse 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 SQL Server e lerá a cadeia de conexão da configuração do ASP.NET Core. Geralmente, não importa em que parte do ConfigureServices a chamada AddDbContext é feita.

A classe ApplicationDbContext deve expor um construtor público com um parâmetro DbContextOptions<ApplicationDbContext>. É assim que a configuração de contexto AddDbContext é passada para o DbContext. Por exemplo:

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }
}

ApplicationDbContext pode ser usado em controladores do ASP.NET Core ou outros serviços por meio da injeção de construtor. Por exemplo:

public class MyController
{
    private readonly ApplicationDbContext _context;

    public MyController(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.

Leia mais partes deste artigo para saber mais sobre as opções de configuração. Além disso, confira Inicialização de aplicativo em ASP.NET Core e Injeção de dependência no ASP.NET Core para obter mais informações sobre a configuração e injeção de dependência no ASP.NET Core.

Inicialização simples do DbContext com 'new'

As instâncias DbContext podem ser construídas conforme 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. Por exemplo:

public class ApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test");
    }
}

Esse padrão também facilita a passagem da configuração como a cadeia de conexão por meio do construtor DbContext. Por exemplo:

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, o 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);

Uso de um alocador DbContext (por exemplo, para Blazor)

Alguns tipos de aplicativo (por exemplo, ASP.NET Core Blazor) usam injeção de dependência, mas não criam um escopo de serviço que se alinha com o tempo de vida desejado do DbContext. Mesmo quando esse alinhamento existis, talvez o aplicativo precise executar várias unidades de trabalho dentro desse escopo. Por exemplo, várias unidades de trabalho em uma única solicitação HTTP.

Nesses casos, AddDbContextFactory pode ser usado para registrar um alocador para a criação de instâncias DbContext. Por exemplo:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContextFactory<ApplicationDbContext>(
        options =>
            options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test"));
}

A classe ApplicationDbContext deve expor um construtor público com um parâmetro DbContextOptions<ApplicationDbContext>. Esse é o mesmo padrão usado na seção do ASP.NET Core tradicional acima.

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }
}

Em seguida, o alocador DbContextFactory pode ser usado em outros serviços por meio da injeção de construtor. Por exemplo:

private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;

public MyController(IDbContextFactory<ApplicationDbContext> contextFactory)
{
    _contextFactory = contextFactory;
}

Em seguida, o alocador injetado pode ser usado para construir instâncias DbContext no código de serviço. Por exemplo:

public void DoSomething()
{
    using (var context = _contextFactory.CreateDbContext())
    {
        // ...
    }
}

Observe que as instâncias DbContext criadas dessa forma não são gerenciadas pelo provedor de serviços de aplicativo e, portanto, devem ser descartadas pelo aplicativo.

Confira ASP.NET Core Blazor Server com o Entity Framework Core para obter mais informações sobre como usar o EF Core com o Blazor.

DbContextOptions

O ponto de partida de toda a configuração de DbContext é DbContextOptionsBuilder. Há três maneiras de obter esse construtor:

  • No AddDbContext e métodos relacionados
  • Em OnConfiguring
  • Construído explicitamente com new

Exemplos de cada um deles são mostrados nas seções anteriores. A mesma configuração pode ser aplicada independentemente de onde vem o construtor. Além disso, o 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.

Configuração do provedor de banco de dados

Cada instância DbContext deve ser configurada para usar apenas um provedor de banco de dados. (Instâncias diferentes de um subtipo DbContext podem ser usadas com provedores de banco de dados diferentes, mas uma única instância deve usar apenas uma.) Um provedor de banco de dados é configurado usando uma chamada específica Use*. Por exemplo, para usar o provedor de banco de dados do SQL Server:

public class ApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test");
    }
}

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.

Dica

Os provedores de banco de dados do EF Core fazem uso extensivo dos métodos de extensão. Se o compilador indicar que um método não pode ser encontrado, verifique se o pacote NuGet do provedor está instalado e se using Microsoft.EntityFrameworkCore; está no seu código.

A tabela a seguir contém exemplos para provedores de banco de dados comuns.

Sistema de banco de dados Configuração de exemplo Pacote NuGet
SQL Server ou SQL do Azure .UseSqlServer(connectionString) Microsoft.EntityFrameworkCore.SqlServer
Azure Cosmos DB .UseCosmos(connectionString, databaseName) Microsoft.EntityFrameworkCore.Cosmos
SQLite .UseSqlite(connectionString) Microsoft.EntityFrameworkCore.Sqlite
Banco de dados em memória do EF Core .UseInMemoryDatabase(databaseName) Microsoft.EntityFrameworkCore.InMemory
PostgreSQL* .UseNpgsql(connectionString) Npgsql.EntityFrameworkCore.PostgreSQL
MySQL/MariaDB* .UseMySql(connectionString) Pomelo.EntityFrameworkCore.MySql
Oracle* .UseOracle(connectionString) Oracle.EntityFrameworkCore

*Esses provedores de banco de dados não são enviados pela Microsoft. Confira Provedores de banco de dados para obter mais informações sobre provedores de banco de dados.

Aviso

O banco de dados na memória do EF Core não foi projetado para ser usado em produção. Além disso, ele pode não ser a melhor opção nem mesmo para teste. Confira Código de teste que usa o EF Core para obter mais informações.

Confira Cadeias de conexão para obter mais informações sobre como usar cadeias de conexão com o EF Core.

A configuração opcional específica para o provedor de banco de dados é executada em um construtor específico do provedor adicional. Por exemplo, usar EnableRetryOnFailure para configurar novas tentativas de resiliência de conexão ao se conectar ao SQL do Azure:

public class ApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer(
                @"Server=(localdb)\mssqllocaldb;Database=Test",
                providerOptions => { providerOptions.EnableRetryOnFailure(); });
    }
}

Dica

O mesmo provedor de banco de dados é usado para o SQL Server e o SQL do Azure. No entanto, é recomendável que a resiliência de conexão seja usada ao se conectar ao SQL do Azure.

Confira Provedores de banco de dados para obter mais informações sobre a configuração específica do provedor.

Outra configuração do DbContext

Outra configuração DbContext pode ser encadeada antes ou depois (não faz diferença qual) da chamada Use*. Por exemplo, para ativar o registro de log de dados confidenciais:

public class ApplicationDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .EnableSensitiveDataLogging()
            .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test");
    }
}

A tabela a seguir contém exemplos de métodos comuns chamados em DbContextOptionsBuilder.

Método DbContextOptionsBuilder O que faz Saiba mais
UseQueryTrackingBehavior Define o comportamento de acompanhamento padrão para consultas Comportamento de acompanhamento de consulta
LogTo Uma forma simples de obter logs do EF Core (EF Core 5.0 e posterior) Registro em log, eventos e diagnóstico
UseLoggerFactory Registra um alocador Microsoft.Extensions.Logging Registro em log, eventos e diagnóstico
EnableSensitiveDataLogging Inclui dados do aplicativo em exceções e registro em log Registro em log, eventos e diagnóstico
EnableDetailedErrors Erros de consulta mais detalhados (em detrimento ao desempenho) Registro em log, eventos e diagnóstico
ConfigureWarnings Ignora ou lança avisos e outros eventos Registro em log, eventos e diagnóstico
AddInterceptors Registra interceptadores do EF Core Registro em log, eventos e diagnóstico
UseLazyLoadingProxies Usa proxies dinâmicos para carregamento lento Carregamento lento
UseChangeTrackingProxies Usa proxies dinâmicos para acompanhamento de alterações Em breve...

Observação

UseLazyLoadingProxies e UseChangeTrackingProxies são métodos de extensão do pacote NuGet Microsoft.EntityFrameworkCore.Proxies. Esse tipo de chamada ".UseSomething()" é a maneira recomendada de configurar e/ou usar extensões EF Core contidas em outros pacotes.

DbContextOptions versus DbContextOptions<TContext>

A maioria das subclasses DbContext que aceitam um DbContextOptions deve usar a variação genéricaDbContextOptions<TContext>. Por exemplo:

public sealed class SealedApplicationDbContext : DbContext
{
    public SealedApplicationDbContext(DbContextOptions<SealedApplicationDbContext> contextOptions)
        : base(contextOptions)
    {
    }
}

Isso garante que as opções corretas para o subtipo DbContext específico sejam resolvidas com base na injeção de dependência, mesmo quando há vários subtipos DbContext registrados.

Dica

Seu DbContext não precisa ser selado, mas a selagem é a melhor prática para fazer isso em classes que não são projetadas para serem herdadas.

No entanto, caso o subtipo DbContext destine-se a ser herdado, ele deve expor um construtor protegido que usa um DbContextOptions não genérico. Por exemplo:

public abstract class ApplicationDbContextBase : DbContext
{
    protected ApplicationDbContextBase(DbContextOptions contextOptions)
        : base(contextOptions)
    {
    }
}

Isso permite que várias subclasses concretas chamem esse construtor base usando suas diferentes instâncias DbContextOptions<TContext> genéricas. Por exemplo:

public sealed class ApplicationDbContext1 : ApplicationDbContextBase
{
    public ApplicationDbContext1(DbContextOptions<ApplicationDbContext1> contextOptions)
        : base(contextOptions)
    {
    }
}

public sealed class ApplicationDbContext2 : ApplicationDbContextBase
{
    public ApplicationDbContext2(DbContextOptions<ApplicationDbContext2> contextOptions)
        : base(contextOptions)
    {
    }
}

Observe que esse é exatamente o mesmo padrão de ao herdar diretamente de DbContext. Ou seja, o construtor DbContext em si aceita um DbContextOptions não genérico por esse motivo.

Uma subclasse DbContext destinada a ser instanciada e herdada deve expor ambas as formas de construtor. Por exemplo:

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> contextOptions)
        : base(contextOptions)
    {
    }

    protected ApplicationDbContext(DbContextOptions contextOptions)
        : base(contextOptions)
    {
    }
}

Configuração do DbContext em tempo de design

As ferramentas de tempo de design do EF Core, como aquelas para Migrações do EF Core, precisam ser capazes de descobrir e criar uma instância de trabalho de um tipo DbContext para obter detalhes sobre os tipos de entidade do aplicativo e como eles são mapeados para um esquema de banco de dados. Esse processo pode ser automático, desde que a ferramenta possa criar o DbContext facilmente de um modo em que ele será configurado de modo semelhante a como ele seria configurado em tempo de execução.

Embora qualquer padrão que forneça as informações de configuração necessárias ao DbContext possa funcionar em tempo de execução, as ferramentas que exigem o uso de um DbContext no tempo de design só podem funcionar com um número limitado de padrões. Elas são abordadas com mais detalhes na Criação de contexto em tempo de design.

Evitar problemas de threading do DbContext

O Entity Framework Core não oferece suporte para várias operações simultâneas 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 await chamadas assíncronas imediatamente ou use instâncias DbContext separadas para operações executadas em paralelo.

Quando o EF Core detectar uma tentativa de usar uma instância DbContext simultaneamente, você verá uma InvalidOperationException com uma mensagem como esta:

Uma segunda operação foi iniciada nesse contexto antes de uma operação anterior ser concluída. Isso geralmente ocorre devido a threads diferentes estarem usando a mesma instância do DbContext, no entanto, os membros da instância não têm a garantia de serem thread safe.

Quando o acesso simultâneo não for detectado, isso pode resultar em comportamento indefinido, falhas de aplicativo e corrupção de dados.

Há erros comuns que podem, inadvertidamente, gerar o acesso simultâneo na mesma instância DbContext:

Armadilhas da operação assíncrona

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

Sempre aguarde os métodos assíncronos do EF Core imediatamente.

Compartilhamento implícito de instâncias DbContext por meio de injeção de dependência

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

Isso é seguro contra problemas de acesso simultâneos na maioria dos aplicativos ASP.NET Core porque há apenas um thread executando cada solicitação de 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 de usuário do Blazor e, portanto, apenas uma instância DbContext com escopo estará disponível por circuito de usuário se o escopo de injeção padrão for usado.

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

Usando a injeção de dependência, isso pode ser obtido ao registrar o contexto como escopo e ao criar escopos (usando IServiceScopeFactory) para cada thread ou registrando o DbContext como transitório (usando a sobrecarga do AddDbContext, a qual usa um parâmetro ServiceLifetime).

Mais leituras