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 em uma única unidade 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;ConnectRetryCount=0");
}
}
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;ConnectRetryCount=0")
.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;ConnectRetryCount=0"));
}
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;ConnectRetryCount=0");
}
}
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;ConnectRetryCount=0");
}
}
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 maneira simples de obter logs do EF Core | 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érica DbContextOptions<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
- Leia Injeção de dependência para saber mais sobre como usar a DI.
- Leia Teste para obter mais informações.