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 di DbContext è progettata per essere usata per una singolaunità 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 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");
    }
}

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")
    .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"));
}

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");
    }
}

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
Oracle* .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");
    }
}

La tabella seguente contiene esempi di metodi comuni chiamati su DbContextOptionsBuilder.

Metodo DbContextOptionsBuilder Funzione 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 sottoclassi DbContext che accetta un oggetto 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