Utiliser le pooling des contextes de données
Introduction
Dans une application ASP.NET Core, un contexte de données (DbContext), enregistré avec la méthode AddDbContext, a généralement une durée de vie liée à la requête HTTP. Dans cette configuration, il peut être utilisé pour exécuter plusieurs requêtes SQL sur la base de données, puis il est libéré à la fin de cette requête. Sa création ne déclenche pas, à elle seule, d’accès à la base de données. Elle implique toutefois de créer l’instance du contexte, et de préparer les composants associés à son fonctionnement, notamment ceux utilisés pour le suivi des entités (Change Tracker).
Lorsque cette opération est répétée très souvent, son coût peut devenir mesurable. Le pooling des contextes de données (DbContext) permet de réutiliser des instances déjà initialisées. Cela permet d'optimiser les performances lorsque l’application traite un grand nombre de requêtes ou cherche à réduire ses allocations mémoire.
Comment fonctionne le pooling ?
Sans pooling, l’application obtient un nouveau contexte de données (DbContext) à chaque requête HTTP. Avec le pooling, EF Core conserve des instances dans un pool. Lorsqu’un contexte est libéré, EF Core réinitialise son état interne pour le mettre de nouveau à disposition pour une utilisation ultérieure.
Le contexte reste réservé à une seule utilisation à la fois. Le pooling ne permet pas de partager une instance entre plusieurs traitements : un DbContext ne doit toujours pas être utilisé en parallèle par plusieurs threads.
Activer le pooling dans ASP.NET Core
Nous développons un contexte de données utilisé pour consulter des commandes :
using Microsoft.EntityFrameworkCore;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(
DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Order> Orders => Set<Order>();
}
L'enregistrement dans le conteneur d'injection de dépendances dans la classe Program, via la méthode AddDbContext :
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")));
Pour activer le pooling, utilisons la méthode AddDbContextPool :
builder.Services.AddDbContextPool<ApplicationDbContext>(
options => options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")));
Le code qui a besoin d'un contexte de données continue d'injecter la classe ApplicationDbContext dans le constructeur par injection de dépendances :
public class OrderService
{
private readonly ApplicationDbContext _dbContext;
public OrderService(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
public Task<Order?> GetOrderAsync(
int orderId,
CancellationToken cancellationToken)
{
return _dbContext.Orders
.SingleOrDefaultAsync(
order => order.Id == orderId,
cancellationToken);
}
}
La taille maximale du pool de contextes de données peut être précisée avec le paramètre poolSize (valeur par défaut : 1024). Elle limite le nombre de contextes conservés pour réutilisation. Toutefois, si davantage de contextes sont nécessaires, EF Core peut en créer d’autres, mais elles ne seront pas conservées dans le pool.
builder.Services.AddDbContextPool<ApplicationDbContext>(
options => options.UseSqlServer(
builder.Configuration.GetConnectionString("DefaultConnection")),
poolSize: 128);
Ne pas confondre pooling des contextes de données et pooling des connexions à la base de données
Ces deux mécanismes sont indépendants et répondent à des coûts différents.
| Eléments utilisés | Gains attendus sur les performances | |
|---|---|---|
| Pooling des contextes de données | Les instances de contexte de données utilisent les connexions à la base de données. | Les gains attendus portent sur leur création et initialisation. |
| Pooling des les connexions à la base de données | Les connexions sont gérées par le driver utilisé pour accéder à la base de données. | Les gains attendus portent sur leur ouverture. |
Activer le pooling des contextes de données n'implique pas que des connexions restent ouvertes pendant toute leur présence dans le pool. EF Core ouvre une connexion avant une opération sur la base de données et la ferme ensuite, ce qui permet au driver de la rendre à son propre pool de connexions.
Attention à l’état des contextes de données
Une instance du contexte de données issue du pool peut servir successivement à plusieurs requêtes HTTP. Il faut donc être attentif à tout état ajouté au DbContext par l’application : identifiant du Tenant, informations concernant l’utilisateur courant, etc.
Par exemple, dans une application multi-tenants, plusieurs clients ou organisations utilisent la même application, avec des données isolées dans la base de données. Si le contexte de données (DbContext) contient un identifiant de tenant, celui-ci doit recevoir la valeur attendue à chaque utilisation d’une instance issue du pool. Un service scoped injecté dans le constructeur du contexte ne sera pas réinjecté lors de chaque réutilisation. C'est pourquoi, implémenter une fabrique liée à la portée de la requête est une bonne pratique pour fournir le contexte du pool après lui avoir affecté cet identifiant.
La configuration du contexte doit également rester stable entre ses utilisations. Avec l'utilisation de la méthode AddDbContextPool, les options doivent être configurées lors de l’enregistrement du service. La méthode OnConfiguring n’est pas appelée lors de chaque utilisation d’une instance du pool.
Dans quels cas cette optimisation est-elle utile ?
Activer le pooling de contextes de données (DbContext) peut améliorer les performances lorsque la création répétée des contextes représente une part sensible du temps de traitement, notamment dans une application dans laquelle les requêtes Linq To Entities exécutées sur la base de données ont une durée d'exécution très courte (incluant l'exécution de la requête SQL, le transfert des données entre le serveur de base de données et l'application et la construction / suivi des objets dans le contexte de données). Implémenter un benchmark mettant en relief un gain de performances est utile pour décider d'activer ou non le pooling sur le contexte de données.