Durata, configurazione e inizializzazione di DbContext
Questo articolo illustra i modelli di base per l'inizializzazione e la configurazione di un'istanza di DbContext.
Durata di DbContext
La durata di DbContext
inizia quando l'istanza viene creata e termina quando l'istanza viene eliminata. Un'istanza DbContext
di è progettata per essere usata per una singola unità di lavoro. Ciò significa che la durata di un'istanza di DbContext
è in genere molto breve.
Suggerimento
Per citare Martin Fowler dal collegamento precedente, "Un'unità di lavoro tiene traccia di tutto ciò che viene eseguito durante una transazione commerciale che può influire sul database. Al termine, stabilisce tutto ciò che deve essere fatto per modificare il database in seguito al lavoro svolto.
Un'unità di lavoro tipica quando si usa Entity Framework Core (EF Core) implica le operazioni seguenti:
- Creazione di un'istanza di
DbContext
- Rilevamento delle istanze di entità dal contesto. Le entità vengono rilevate da
- Vengono restituite da una query
- Vengonoaggiunte o collegate al contesto
- Vengono apportate modifiche alle entità rilevate in base alle esigenze per implementare la regola di business
- Viene chiamato SaveChanges o SaveChangesAsync. EF Core rileva le modifiche apportate e le scrive nel database.
- Viene eliminata l'istanza di
DbContext
Importante
- È molto importante eliminare DbContext dopo l'uso. Ciò garantisce che tutte le risorse non gestite vengano liberate e che venga annullata la registrazione di eventuali eventi o altri hook, in modo da evitare perdite di memoria nel caso in cui venga mantenuto il riferimento all'istanza.
- DbContext non è thread-safe. Non condividere contesti tra thread. Assicurarsi di attendere tutte le chiamate asincrone prima di continuare a usare l'istanza del contesto.
- Un'eccezione InvalidOperationException generata dal codice EF Core può rendere il contesto non ripristinabile. Tali eccezioni indicano un errore del programma e non sono progettate per essere ripristinate.
DbContext nell'inserimento delle dipendenze per ASP.NET Core
In molte applicazioni Web ogni richiesta HTTP corrisponde a una singola unità di lavoro. Questo fa sì che legare durata del contesto a quello della richiesta sia un buon approccio per le applicazioni Web.
Le applicazioni ASP.NET Core vengono configurate usando l'inserimento delle dipendenze. EF Core può essere aggiunto a questa configurazione usando AddDbContext nel metodo ConfigureServices
di Startup.cs
. Ad esempio:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<ApplicationDbContext>(
options => options.UseSqlServer("name=ConnectionStrings:DefaultConnection"));
}
In questo esempio viene registrata una sottoclasse DbContext
denominata ApplicationDbContext
come servizio con ambito nel provider di servizi dell'applicazione ASP.NET Core (anche noto come contenitore di inserimento delle dipendenze). Il contesto è configurato per usare il provider di database SQL Server e leggerà la stringa di connessione dalla configurazione di ASP.NET Core. In genere non importa dove in ConfigureServices
viene effettuata la chiamata a AddDbContext
.
La classe ApplicationDbContext
deve esporre un costruttore pubblico con un parametro DbContextOptions<ApplicationDbContext>
. In questo modo, la configurazione del contesto da AddDbContext
viene passata a DbContext
. Ad esempio:
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
ApplicationDbContext
può quindi essere usato nei controller di ASP.NET Core o in altri servizi tramite l'inserimento del costruttore. Ad esempio:
public class MyController
{
private readonly ApplicationDbContext _context;
public MyController(ApplicationDbContext context)
{
_context = context;
}
}
Il risultato finale è un'istanza di ApplicationDbContext
creata per ogni richiesta e passata al controller per eseguire un'unità di lavoro prima di essere eliminata al termine della richiesta.
Per altre informazioni sulle opzioni di configurazione, continuare a leggere questo articolo. Inoltre, per altre informazioni sulla configurazione e l'inserimento delle dipendenze in ASP.NET Core, vedere Avvio dell'app in ASP.NET Core e Inserimento delle dipendenze in ASP.NET Core.
Inizializzazione semplice di DbContext con 'new'
Le istanze di DbContext
possono essere create nel modo normale di .NET, ad esempio con new
in C#. La configurazione può essere eseguita eseguendo l'override del metodo OnConfiguring
o passando le opzioni al costruttore. Ad esempio:
public class ApplicationDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test;ConnectRetryCount=0");
}
}
Questo modello semplifica anche il passaggio della configurazione come la stringa di connessione tramite il costruttore DbContext
. Ad esempio:
public class ApplicationDbContext : DbContext
{
private readonly string _connectionString;
public ApplicationDbContext(string connectionString)
{
_connectionString = connectionString;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(_connectionString);
}
}
In alternativa, è possibile usare DbContextOptionsBuilder
per creare un oggetto DbContextOptions
che viene quindi passato al costruttore DbContext
. Ciò consente di costruire in modo esplicito anche un oggetto DbContext
configurato per l'inserimento delle dipendenze. Ad esempio, quando si usa ApplicationDbContext
definito per le app Web ASP.NET Core precedenti:
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
È possibile creare DbContextOptions
e chiamare il costruttore in modo esplicito:
var contextOptions = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test;ConnectRetryCount=0")
.Options;
using var context = new ApplicationDbContext(contextOptions);
Uso di una factory DbContext (ad esempio, per Blazor)
Alcuni tipi di applicazione (ad esempio, ASP.NET Core Blazor) usano l'inserimento delle dipendenze, ma non creano un ambito di servizio che si allinei alla durata desiderata di DbContext
. Anche quando esiste un tale allineamento, l'applicazione potrebbe dover eseguire più unità di lavoro all'interno di questo ambito. Ad esempio, più unità di lavoro all'interno di una singola richiesta HTTP.
In questi casi, è possibile usare AddDbContextFactory per registrare una factory per la creazione di istanze di DbContext
. Ad esempio:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContextFactory<ApplicationDbContext>(
options =>
options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test;ConnectRetryCount=0"));
}
La classe ApplicationDbContext
deve esporre un costruttore pubblico con un parametro DbContextOptions<ApplicationDbContext>
. Si tratta dello stesso modello usato nella sezione ASP.NET Core tradizionale precedente.
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
La factory DbContextFactory
può quindi essere usata in altri servizi tramite l'inserimento del costruttore. Ad esempio:
private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;
public MyController(IDbContextFactory<ApplicationDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
La factory inserita può quindi essere usata per costruire istanze di DbContext nel codice del servizio. Ad esempio:
public void DoSomething()
{
using (var context = _contextFactory.CreateDbContext())
{
// ...
}
}
Si noti che le istanze di DbContext
create in questo modo non vengono gestite dal provider di servizi dell'applicazione e pertanto devono essere eliminate dall'applicazione.
Vedere ASP.NET Core Blazor Server con Entity Framework Core per altre informazioni sull'uso di EF Core con Blazor.
DbContextOptions
Il punto iniziale per tutte le configurazioni di DbContext
è DbContextOptionsBuilder. Esistono tre modi per ottenere questo generatore:
- In
AddDbContext
e metodi correlati - In
OnConfiguring
- Costruito in modo esplicito con
new
Gli esempi di ognuno di questi sono illustrati nelle sezioni precedenti. La stessa configurazione può essere applicata indipendentemente dalla provenienza del generatore. Inoltre, OnConfiguring
viene sempre chiamato indipendentemente dal modo in cui viene costruito il contesto. Questo significa che OnConfiguring
può essere usato per eseguire una configurazione aggiuntiva anche quando viene usato AddDbContext
.
Configurazione del provider di database
Ogni istanza di DbContext
deve essere configurata per usare un solo provider di database. Diverse istanze di un sottotipo DbContext
possono essere usate con diversi provider di database, ma una singola istanza deve usarne solo uno. Un provider di database viene configurato usando una chiamata Use*
specifica. Ad esempio, per usare il provider di database SQL Server:
public class ApplicationDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test;ConnectRetryCount=0");
}
}
Questi metodi Use*
sono metodi di estensione implementati dal provider di database. Ciò significa che il pacchetto NuGet del provider di database deve essere installato prima di poter usare il metodo di estensione.
Suggerimento
I provider di database EF Core fanno largo uso di metodi di estensione. Se il compilatore indica che non è possibile trovare un metodo, assicurarsi che il pacchetto NuGet del provider sia installato e che sia presente using Microsoft.EntityFrameworkCore;
nel codice.
La tabella seguente contiene esempi per i provider di database comuni.
Sistema di database | Configurazione di esempio | Pacchetto NuGet |
---|---|---|
SQL Server o Azure SQL | .UseSqlServer(connectionString) |
Microsoft.EntityFrameworkCore.SqlServer |
Azure Cosmos DB | .UseCosmos(connectionString, databaseName) |
Microsoft.EntityFrameworkCore.Cosmos |
SQLite | .UseSqlite(connectionString) |
Microsoft.EntityFrameworkCore.Sqlite |
Database in memoria EF Core | .UseInMemoryDatabase(databaseName) |
Microsoft.EntityFrameworkCore.InMemory |
PostgreSQL* | .UseNpgsql(connectionString) |
Npgsql.EntityFrameworkCore.PostgreSQL |
MySQL/MariaDB* | .UseMySql(connectionString) |
Pomelo.EntityFrameworkCore.MySql |
Oracolo* | .UseOracle(connectionString) |
Oracle.EntityFrameworkCore |
*Questi provider di database non vengono forniti da Microsoft. Per altre informazioni sui provider di database, vedere Provider di database.
Avviso
Il database EF Core in memoria non è progettato per l'uso in produzione. Inoltre, potrebbe non essere la scelta migliore neanche per i test. Per altre informazioni, vedere Test del codice che usa EF Core.
Per altre informazioni sull'uso delle stringhe di connessione con EF Core, vedere Stringhe di connessione.
La configurazione facoltativa specifica del provider di database viene eseguita in un generatore aggiuntivo specifico del provider. Ad esempio, usando EnableRetryOnFailure per configurare i tentativi per la resilienza della connessione quando ci si connette ad Azure SQL:
public class ApplicationDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=Test",
providerOptions => { providerOptions.EnableRetryOnFailure(); });
}
}
Suggerimento
Lo stesso provider di database viene usato per SQL Server e Azure SQL. È tuttavia consigliabile usare la resilienza della connessione quando ci si connette a SQL Azure.
Per altre informazioni sulla configurazione specifica del provider, vedere Provider di database.
Altre configurazioni di DbContext
È possibile concatenare un'altra configurazione di DbContext
prima o dopo la chiamata Use*
(non è importante l'ordine). Ad esempio, per attivare la registrazione dei dati sensibili:
public class ApplicationDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.EnableSensitiveDataLogging()
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test;ConnectRetryCount=0");
}
}
La tabella seguente contiene esempi di metodi comuni chiamati su DbContextOptionsBuilder
.
Metodo DbContextOptionsBuilder | Risultato | Altre informazioni |
---|---|---|
UseQueryTrackingBehavior | Imposta il comportamento di rilevamento predefinito per le query | Comportamento di rilevamento query |
LogTo | Un modo semplice per ottenere i log di EF Core | Registrazione, eventi e diagnostica |
UseLoggerFactory | Registra una factory di Microsoft.Extensions.Logging |
Registrazione, eventi e diagnostica |
EnableSensitiveDataLogging | Include i dati dell'applicazione nelle eccezioni e nella registrazione | Registrazione, eventi e diagnostica |
EnableDetailedErrors | Errori di query più dettagliati (a spese delle prestazioni) | Registrazione, eventi e diagnostica |
ConfigureWarnings | Viene ignorato o generato per avvisi e altri eventi | Registrazione, eventi e diagnostica |
AddInterceptors | Registra gli intercettori EF Core | Registrazione, eventi e diagnostica |
UseLazyLoadingProxies | Usare proxy dinamici per il caricamento lazy | Caricamento lazy |
UseChangeTrackingProxies | Usare proxy dinamici per il rilevamento delle modifiche | Presto disponibile... |
Nota
UseLazyLoadingProxies e UseChangeTrackingProxies sono metodi di estensione del pacchetto NuGet Microsoft.EntityFrameworkCore.Proxies. Questo tipo di chiamata ".UseSomething()" è il modo consigliato per configurare e/o usare le estensioni EF Core contenute in altri pacchetti.
DbContextOptions
e DbContextOptions<TContext>
La maggior parte delle DbContext
sottoclassi che accettano un DbContextOptions
deve usare la variante genericaDbContextOptions<TContext>
. Ad esempio:
public sealed class SealedApplicationDbContext : DbContext
{
public SealedApplicationDbContext(DbContextOptions<SealedApplicationDbContext> contextOptions)
: base(contextOptions)
{
}
}
Ciò garantisce che le opzioni corrette per il sottotipo specifico DbContext
vengano risolte dall'inserimento delle dipendenze, anche quando vengono registrati più sottotipi DbContext
.
Suggerimento
Non è necessario che DbContext sia sealed, ma è una procedura consigliata per l'ereditarietà dalle classi non progettate.
Tuttavia, se si deve ereditare dal sottotipo DbContext
, quest'ultimo dovrà esporre un costruttore protetto che accetta un oggetto DbContextOptions
non generico. Ad esempio:
public abstract class ApplicationDbContextBase : DbContext
{
protected ApplicationDbContextBase(DbContextOptions contextOptions)
: base(contextOptions)
{
}
}
Ciò consente a più sottoclassi concrete di chiamare questo costruttore di base usando le diverse istanze generiche di DbContextOptions<TContext>
. Ad esempio:
public sealed class ApplicationDbContext1 : ApplicationDbContextBase
{
public ApplicationDbContext1(DbContextOptions<ApplicationDbContext1> contextOptions)
: base(contextOptions)
{
}
}
public sealed class ApplicationDbContext2 : ApplicationDbContextBase
{
public ApplicationDbContext2(DbContextOptions<ApplicationDbContext2> contextOptions)
: base(contextOptions)
{
}
}
Si noti che questo è esattamente lo stesso modello di quando si eredita direttamente da DbContext
. Vale a dire che il costruttore DbContext
stesso accetta un oggetto DbContextOptions
non generico per questo motivo.
Una sottoclasse DbContext
di cui deve essere creata un'istanza e da cui si deve ereditare dovrà esporre entrambe i formati del costruttore. Ad esempio:
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> contextOptions)
: base(contextOptions)
{
}
protected ApplicationDbContext(DbContextOptions contextOptions)
: base(contextOptions)
{
}
}
Configurazione di DbContext in fase di progettazione
Gli strumenti di progettazione di EF Core, come quelli per le migrazioni di EF Core, devono poter individuare e creare un'istanza funzionante di un tipo DbContext
per raccogliere informazioni dettagliate sui tipi di entità dell'applicazione e su come eseguire il mapping a uno schema di database. Questo processo può essere automatico a condizione che lo strumento possa creare facilmente DbContext
affinché venga configurato in modo analogo a come verrebbe configurato in fase di esecuzione.
Mentre qualsiasi modello che fornisce le informazioni di configurazione necessarie a DbContext
può funzionare in fase di esecuzione, gli strumenti che richiedono l'utilizzo di DbContext
in fase di progettazione possono funzionare solo con un numero limitato di modelli. Questi sono illustrati in dettaglio nell'argomento Creazione del contesto in fase di progettazione.
Evitare problemi di threading di DbContext
Entity Framework Core non supporta più operazioni parallele in esecuzione nella stessa istanza di DbContext
. Ciò include sia l'esecuzione parallela di query asincrone che qualsiasi uso simultaneo esplicito da più thread. Pertanto, eseguire sempre immediatamente le chiamate asincrone await
o usare istanze separate di DbContext
per le operazioni che vengono eseguite in parallelo.
Quando EF Core rileva un tentativo di usare un'istanza di DbContext
simultaneamente, verrà visualizzata un'eccezione InvalidOperationException
con un messaggio simile al seguente:
Una seconda operazione è stata avviata in questo contesto prima del completamento di un'operazione precedente. Ciò è in genere causato da thread diversi che usano la stessa istanza di DbContext, tuttavia non è garantito che i membri dell'istanza siano thread-safe.
Quando l'accesso simultaneo non viene rilevato, può causare un comportamento non definito, arresti anomali dell'applicazione e danneggiamento dei dati.
Esistono errori comuni che possono causare inavvertitamente l'accesso simultaneo nella stessa istanza di DbContext
:
Problemi delle operazioni asincrone
I metodi asincroni consentono a EF Core di avviare operazioni che accedono al database in modo non bloccante. Tuttavia, se un chiamante non attende il completamento di uno di questi metodi e continua a eseguire altre operazioni su DbContext
, lo stato di DbContext
può risultare danneggiato (e molto probabilmente lo sarà).
Attendere sempre immediatamente i metodi asincroni di EF Core.
Condivisione implicita delle istanze di DbContext tramite inserimento delle dipendenze
Il metodo di estensione AddDbContext
registra i tipi DbContext
con una durata con ambito per impostazione predefinita.
Questo è al sicuro da problemi di accesso simultaneo nella maggior parte delle applicazioni ASP.NET Core perché è presente un solo thread che esegue ogni richiesta client in un determinato momento e perché ogni richiesta ottiene un ambito di inserimento delle dipendenze separato (e quindi un'istanza separata di DbContext
). Per il modello di hosting di Blazor Server, viene usata una richiesta logica per mantenere il circuito utente Blazor e quindi è disponibile una sola istanza di DbContext con ambito per circuito utente se viene usato l'ambito di inserimento predefinito.
Qualsiasi codice che esegue in modo esplicito più thread in parallelo deve assicurarsi che le istanze di DbContext
non siano mai accessibili simultaneamente.
L'uso dell'inserimento delle dipendenze può essere ottenuto registrando il contesto come ambito e creando ambiti (usando IServiceScopeFactory
) per ogni thread o registrando DbContext
come temporaneo (usando l'overload di AddDbContext
che accetta un parametro ServiceLifetime
).
Altri riferimenti
- Per altre informazioni sull'uso dell'inserimento delle dipendenze, vedere Inserimento delle dipendenze.
- Per altre informazioni, vedere Test.