Durée de vie, configuration et initialisation de DbContext

Cet article présente des modèles de base pour l’initialisation et la configuration d’une instance DbContext.

Durée de vie de DbContext

La durée de vie de DbContext commence lorsque l'instance est créée et se termine lorsque l'instance est supprimée. Une instance DbContext est conçue pour être utilisée pour une seuleunité de travail. Cela signifie que la durée de vie d’une instance DbContext est généralement très courte.

Conseil

Pour citer Martin Fowler à partir du lien ci-dessus, « Une unité de travail effectue le suivi de tout ce que vous faites lors d’une transaction commerciale qui peut affecter la base de données. Lorsque vous avez terminé, il détermine tout ce qui doit être fait pour modifier la base de données à la suite de votre travail.

Une unité de travail classique lors de l’utilisation d’Entity Framework Core (EF Core) implique :

  • Création d’une instance DbContext
  • Suivi des instances d’entité par le contexte. Les entités deviennent suivies par
  • Les modifications sont apportées aux entités suivies selon les besoins pour implémenter la règle métier
  • SaveChanges ou SaveChangesAsync est appelé. EF Core détecte les modifications apportées et les écrit dans la base de données.
  • L’instance DbContext est supprimée

Important

  • Il est très important de supprimer DbContext après utilisation. Cela garantit que toutes les ressources non managées sont libérées et que tous les événements ou autres crochets ne sont pas enregistrés afin d’empêcher les fuites de mémoire dans le cas où l’instance reste référencée.
  • DbContext n’est pas thread-safe. Ne partagez pas de contextes entre les threads. Veillez à attendre tous les appels asynchrones avant de continuer à utiliser l’instance de contexte.
  • Un InvalidOperationException levé par un code EF Core peut placer le contexte dans un état irrécupérable. Ces exceptions indiquent une erreur de programme et ne sont pas conçues pour être récupérées.

DbContext dans l’injection de dépendances pour ASP.NET Core

Dans de nombreuses applications web, chaque requête HTTP correspond à une seule unité de travail. Cela rend la durée de vie du contexte liée à celle de la requête une bonne valeur par défaut pour les applications web.

Les applications ASP.NET Core sont configurées à l’aide de l’injection de dépendances. EF Core peut être ajouté à cette configuration à l’aide de AddDbContext dans la méthode ConfigureServices de Startup.cs. Par exemple :

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

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

Cet exemple enregistre une sous-classe DbContext appelée ApplicationDbContext en tant que service délimité dans le fournisseur de services d’application ASP.NET Core (a.k.a. le conteneur d’injection de dépendances). Le contexte est configuré pour utiliser le fournisseur de base de données SQL Server et lit la chaîne de connexion à partir de ASP.NET Core configuration. Il n’est généralement pas important dans ConfigureServices l’appel à AddDbContext est effectué.

La classe ApplicationDbContext doit exposer un constructeur public avec un paramètre DbContextOptions<ApplicationDbContext>. Il s’agit de la façon dont la configuration de contexte à partir de AddDbContext est passée au DbContext. Par exemple :

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

ApplicationDbContext peut ensuite être utilisé dans ASP.NET Core contrôleurs ou d’autres services par injection de constructeur. Par exemple :

public class MyController
{
    private readonly ApplicationDbContext _context;

    public MyController(ApplicationDbContext context)
    {
        _context = context;
    }
}

Le résultat final est une instance ApplicationDbContext créée pour chaque requête et transmise au contrôleur pour effectuer une unité de travail avant d’être supprimée lorsque la requête se termine.

Pour en savoir plus sur les options de configuration, consultez cet article. En outre, consultez Démarrage de l’application dans ASP.NET Core et Injection de dépendances dans ASP.NET Core pour plus d’informations sur la configuration et l’injection de dépendances dans ASP.NET Core.

Initialisation simple DbContext avec « new »

Les instances DbContext peuvent être construites de la manière .NET normale, par exemple avec new en C#. La configuration peut être effectuée en remplaçant la méthode OnConfiguring ou en passant des options au constructeur. Par exemple :

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

Ce modèle facilite également la transmission de la configuration comme la chaîne de connexion via le constructeur DbContext. Par exemple :

public class ApplicationDbContext : DbContext
{
    private readonly string _connectionString;

    public ApplicationDbContext(string connectionString)
    {
        _connectionString = connectionString;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer(_connectionString);
    }
}

Vous pouvez également utiliser DbContextOptionsBuilder pour créer un objet DbContextOptions qui est ensuite passé au constructeur DbContext. Cela permet à un DbContext configuré pour l’injection de dépendances d’être construit explicitement. Par exemple, lors de l’utilisation de ApplicationDbContext définie pour les applications web ASP.NET Core ci-dessus :

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

DbContextOptions peut être créé et le constructeur peut être appelé explicitement :

var contextOptions = new DbContextOptionsBuilder<ApplicationDbContext>()
    .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test")
    .Options;

using var context = new ApplicationDbContext(contextOptions);

Utilisation d’une fabrique DbContext (par exemple, pour Blazor)

Certains types d’application (par exemple, ASP.NET Core Blazor) utilisent l’injection de dépendances, mais ne créent pas d’étendue de service qui s’aligne sur la durée de vie souhaitéeDbContext. Même lorsqu’un tel alignement existe, l’application peut avoir besoin d’effectuer plusieurs unités de travail dans cette étendue. Par exemple, plusieurs unités de travail au sein d’une seule requête HTTP.

Dans ces cas, vous pouvez utiliser AddDbContextFactory pour inscrire une fabrique pour la création d’instances DbContext. Par exemple :

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

La classe ApplicationDbContext doit exposer un constructeur public avec un paramètre DbContextOptions<ApplicationDbContext>. Il s’agit du même modèle que celui utilisé dans la section ASP.NET Core traditionnelle ci-dessus.

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

La fabrique DbContextFactory peut ensuite être utilisée dans d’autres services par injection de constructeur. Par exemple :

private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;

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

La fabrique injectée peut ensuite être utilisée pour construire des instances DbContext dans le code de service. Par exemple :

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

Notez que les instances DbContextcréées de cette façon ne sont pas gérées par le fournisseur de services de l’application et doivent donc être supprimées par l’application.

Consultez ASP.NET Core Blazor Server avec Entity Framework Core pour plus d’informations sur l’utilisation d’EF Core avec Blazor.

DbContextOptions

Le point de départ de toute configuration DbContext est DbContextOptionsBuilder. Il existe trois façons d’utiliser ce générateur :

  • Dans AddDbContext et les méthodes associées
  • Dans OnConfiguring
  • Construit explicitement avec new

Des exemples de chacun d’entre eux sont affichés dans les sections précédentes. La même configuration peut être appliquée, quel que soit l’endroit où provient le générateur. En outre, OnConfiguring est toujours appelé indépendamment de la façon dont le contexte est construit. Cela signifie que OnConfiguring peut être utilisé pour effectuer une configuration supplémentaire même quand AddDbContext est utilisé.

Configuration du fournisseur de base de données

Chaque instance DbContext doit être configurée pour utiliser un et un seul fournisseur de base de données. (Différentes instances d’un sous-type DbContext peuvent être utilisées avec différents fournisseurs de base de données, mais une seule instance ne doit utiliser qu’une seule.) Un fournisseur de base de données est configuré à l’aide d’un appel spécifique Use*. Par exemple, pour utiliser le fournisseur de base de données SQL Server :

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

Ces méthodes Use* sont des méthodes d’extension implémentées par le fournisseur de base de données. Cela signifie que le package NuGet du fournisseur de base de données doit être installé avant que la méthode d’extension puisse être utilisée.

Conseil

Les fournisseurs de base de données EF Core utilisent largement les méthodes d’extension. Si le compilateur indique qu’une méthode est introuvable, vérifiez que le package NuGet du fournisseur est installé et que vous avez using Microsoft.EntityFrameworkCore; dans votre code.

Le tableau suivant contient des exemples pour les fournisseurs de base de données courants.

Système de base de données Exemple de configuration Package NuGet
SQL Server ou SQL Azure .UseSqlServer(connectionString) Microsoft.EntityFrameworkCore.SqlServer
Azure Cosmos DB .UseCosmos(connectionString, databaseName) Microsoft.EntityFrameworkCore.Cosmos
SQLite .UseSqlite(connectionString) Microsoft.EntityFrameworkCore.Sqlite
Fournisseur en mémoire 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

*Ces fournisseurs de base de données ne sont pas expédiés par Microsoft. Pour plus d’informations sur les fournisseurs de base de données, consultez fournisseurs de base de données.

Avertissement

La base de données EF Core en mémoire n’est pas conçue pour une utilisation en production. En outre, il peut ne pas être le meilleur choix même pour les tests. Pour plus d’informations, consultez Test du code qui utilise EF Core.

Pour plus d’informations sur l’utilisation de chaînes de connexion avec EF Core, consultez Chaînes de connexion.

Une configuration facultative spécifique au fournisseur de base de données est effectuée dans un générateur supplémentaire spécifique au fournisseur. Par exemple, l’utilisation de EnableRetryOnFailure pour configurer les nouvelles tentatives de résilience de connexion lors de la connexion à Azure SQL :

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

Conseil

Le même fournisseur de base de données est utilisé pour SQL Server et Azure SQL. Toutefois, il est recommandé d’utiliser la résilience de connexion lors de la connexion à SQL Azure.

Pour plus d’informations sur la configuration spécifique au fournisseur, consultez Fournisseurs de base de données.

Autre configuration DbContext

D’autres configurations DbContext peuvent être chaînées avant ou après (il n’y a aucune différence qui) l’appel Use*. Par exemple, pour activer la journalisation des données sensibles :

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

Le tableau suivant contient des exemples de méthodes courantes appelées sur DbContextOptionsBuilder.

Méthode DbContextOptionsBuilder Qu’est-ce que cela fait ? En savoir plus
UseQueryTrackingBehavior Définit le comportement de suivi par défaut pour les requêtes Comportement du suivi des requêtes
LogTo Un moyen simple d’obtenir les journaux EF Core Journalisation, événements et diagnostics
UseLoggerFactory Inscrit une fabrique Microsoft.Extensions.Logging Journalisation, événements et diagnostics
EnableSensitiveDataLogging Inclut les données d’application dans les exceptions et la journalisation Journalisation, événements et diagnostics
EnableDetailedErrors Erreurs de requête plus détaillées (au détriment des performances) Journalisation, événements et diagnostics
ConfigureWarnings Ignorer ou lever pour les avertissements et autres événements Journalisation, événements et diagnostics
AddInterceptors Inscrit les intercepteurs EF Core Journalisation, événements et diagnostics
UseLazyLoadingProxies Utiliser des proxys dynamiques pour le chargement différé Chargement différé
UseChangeTrackingProxies Utiliser des proxys dynamiques pour le suivi des modifications Bientôt disponible...

Remarque

UseLazyLoadingProxies et UseChangeTrackingProxies sont des méthodes d’extension du package NuGet Microsoft.EntityFrameworkCore.Proxies. Ce genre de l’appel « .UseSomething() » est la méthode recommandée pour configurer et/ou utiliser des extensions EF Core contenues dans d’autres packages.

DbContextOptions contre DbContextOptions<TContext>

La plupart des sous-classes DbContext qui acceptent un DbContextOptions doit utiliser la variante génériqueDbContextOptions<TContext>. Par exemple :

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

Cela garantit que les options correctes pour le sous-type DbContext spécifique sont résolues à partir de l’injection de dépendances, même lorsque plusieurs sous-types DbContext sont inscrits.

Conseil

Votre DbContext n’a pas besoin d’être scellé, mais il est recommandé de le faire pour les classes qui ne sont pas conçues pour être héritées.

Toutefois, si le sous-type DbContext est lui-même destiné à être hérité, il doit exposer un constructeur protégé prenant un constructeur non générique DbContextOptions. Par exemple :

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

Cela permet à plusieurs sous-classes concrètes d’appeler ce constructeur de base à l’aide de leurs différentes instances génériques DbContextOptions<TContext>. Par exemple :

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

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

Notez que c’est exactement le même modèle que lors de l’héritage direct de DbContext. Autrement dit, le constructeur DbContext lui-même accepte un DbContextOptions non générique pour cette raison.

Une sous-classe DbContext destinée à être instanciée et héritée doit exposer les deux formes de constructeur. Par exemple :

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

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

Configuration DbContext au moment du design

Les outils de conception EF Core tels que ceux pour les migrations EF Core doivent être en mesure de découvrir et de créer une instance de travail d’un type DbContext afin de collecter des détails sur les types d’entités de l’application et la façon dont ils mappent à un schéma de base de données. Ce processus peut être automatique tant que l’outil peut facilement créer le DbContext de manière à ce qu’il soit configuré de la même façon qu’il sera configuré au moment de l’exécution.

Bien que n’importe quel modèle qui fournit les informations de configuration nécessaires au DbContext peut fonctionner au moment de l’exécution , les outils qui nécessitent l’utilisation d’un DbContext au moment du design ne peuvent fonctionner qu’avec un nombre limité de modèles. Ceux-ci sont abordés plus en détail dans la création de contexte au moment du design.

Éviter les problèmes de threading DbContext

Entity Framework Core ne prend pas en charge les opérations parallèles multiples en cours d’exécution sur la même instance DbContext. Cela inclut l’exécution parallèle de requêtes asynchrones et toute utilisation simultanée explicite de plusieurs threads. Par conséquent, les appels asynchrones await sont toujours immédiats ou utilisent des instances distinctes DbContext pour les opérations qui s’exécutent en parallèle.

Lorsque EF Core détecte une tentative d’utilisation simultanée d’une instance DbContext, un InvalidOperationException avec un message semblable à celui-ci s’affiche :

Une deuxième opération a démarré sur ce contexte avant la fin d’une opération précédente. Cela est généralement dû à différents threads utilisant la même instance de DbContext, mais les membres de l’instance ne sont pas garantis comme thread-safe.

Lorsque l’accès simultané n’est pas détecté, il peut entraîner un comportement non défini, des incidents d’application et une altération des données.

Il existe des erreurs courantes qui peuvent provoquer par inadvertance un accès simultané sur la même instance DbContext :

Pièges d’opération asynchrone

Les méthodes asynchrones permettent à EF Core de lancer des opérations qui accèdent à la base de données de manière non bloquante. Toutefois, si un appelant n’attend pas l’achèvement de l’une de ces méthodes et procède à d’autres opérations sur le DbContext, l’état du DbContext peut être (et très probablement) endommagé.

Attendez toujours immédiatement les méthodes asynchrones EF Core.

Partage implicite d’instances DbContext via l’injection de dépendances

La méthode d’extension AddDbContext inscrit les types DbContext avec une durée de vie limitée par défaut.

Cela est sans risque contre les problèmes d’accès simultané dans la plupart des applications ASP.NET Core, car il n’existe qu’un seul thread exécutant chaque requête client à un moment donné, et parce que chaque requête obtient une étendue d’injection de dépendance distincte (et donc une instance distincteDbContext). Pour le modèle d’hébergement Blazor Server, une requête logique est utilisée pour maintenir le circuit utilisateur Blazor, et par conséquent, une seule instance DbContext étendue est disponible par circuit utilisateur si l’étendue d’injection par défaut est utilisée.

Tout code qui exécute explicitement plusieurs threads en parallèle doit s’assurer que les instances DbContext ne sont jamais accessibles simultanément.

À l’aide de l’injection de dépendances, cela peut être obtenu en inscrivant le contexte en tant qu’étendue et en créant des étendues (à l’aide de IServiceScopeFactory) pour chaque thread, ou en inscrivant le DbContext comme temporaire (à l’aide de la surcharge de AddDbContext qui prend un paramètre ServiceLifetime).

Plus de lecture