# Durée de Vie de DbContext dans ASP.NET Core : Performance vs Thread Safety en Async > Maîtrisez la gestion de la durée de vie de DbContext dans ASP.NET Core. Apprenez quand utiliser scoped vs transient, comment gérer les opérations async en toute sécurité et optimiser les performances avec le pooling de DbContext. - Published: 2026-09-07 - Updated: 2026-09-07 - Author: Anthony Fillion-Maillet - Tags: dotnet, entity-framework, aspnet-core, async, performance - Reading time: 10 min --- La gestion de la durée de vie de DbContext constitue l'une des sources les plus fréquentes de bugs dans les applications ASP.NET Core. Le DbContext d'Entity Framework Core n'est pas thread-safe, et une mauvaise utilisation lors d'opérations asynchrones entraîne des états corrompus, des conditions de concurrence et des exceptions difficiles à reproduire. > **DbContext n'est pas thread-safe** > > Une même instance de DbContext ne peut pas être utilisée simultanément sur plusieurs threads. Si une méthode async n'est pas attendue avant qu'une autre opération ne démarre sur le même contexte, l'état interne devient corrompu. Cette règle s'applique à toutes les versions d'EF Core, y compris EF Core 9. ## Pourquoi la Durée de Vie Scoped Fonctionne pour les Requêtes Web L'injection de dépendances d'ASP.NET Core enregistre DbContext avec une durée de vie scoped par défaut lors de l'utilisation de `AddDbContext()`. Chaque requête HTTP reçoit sa propre instance de DbContext, et cette instance est disposée à la fin de la requête. Cette approche résout deux problèmes : elle garantit que chaque requête possède un état de base de données isolé, et elle aligne la durée de vie du contexte avec le pattern Unit of Work où les modifications s'accumulent pendant le traitement de la requête et sont commitées ensemble à la fin. ```csharp // Program.cs builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); ``` La durée de vie scoped est appropriée pour les flux requête-réponse standards. Une action de contrôleur ou un handler de Razor Page s'exécute sur un seul thread, et tant que tous les appels async sont correctement attendus, le DbContext reste dans un état cohérent tout au long de la requête. ```csharp // OrderController.cs public class OrderController : ControllerBase { private readonly AppDbContext _context; public OrderController(AppDbContext context) { _context = context; } [HttpPost] public async Task CreateOrder(CreateOrderDto dto) { var order = new Order { CustomerId = dto.CustomerId, Total = dto.Total }; _context.Orders.Add(order); await _context.SaveChangesAsync(); return CreatedAtAction(nameof(GetOrder), new { id = order.Id }, order); } } ``` La contrainte clé : chaque opération async doit se terminer avant que la suivante ne démarre. Le code ci-dessus est correct car `SaveChangesAsync` est attendu avant que la méthode ne retourne. ## Le Problème de Thread Safety dans les Opérations Parallèles Les problèmes émergent lorsque les développeurs tentent de paralléliser les opérations de base de données au sein d'une même requête. Considérons cet exemple défaillant : ```csharp // CASSÉ : Ne pas utiliser public async Task GetDashboard() { var ordersTask = _context.Orders.CountAsync(); var productsTask = _context.Products.CountAsync(); var customersTask = _context.Customers.CountAsync(); // Exécution de trois requêtes sur le même DbContext simultanément await Task.WhenAll(ordersTask, productsTask, customersTask); return Ok(new { Orders = ordersTask.Result, Products = productsTask.Result, Customers = customersTask.Result }); } ``` Ce code démarre trois requêtes async sans attendre chacune individuellement. Les trois opérations s'exécutent simultanément sur la même instance de DbContext, violant les exigences de thread safety d'EF Core. Le résultat est imprévisible : parfois cela fonctionne, parfois cela lève une `InvalidOperationException`, et parfois cela retourne des données incorrectes. La [documentation officielle Microsoft](https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/) indique explicitement que les méthodes async doivent être attendues immédiatement. Le change tracker interne, l'état de connexion et le cache de compilation des requêtes ne sont pas conçus pour un accès concurrent. ## Utilisation de IDbContextFactory pour les Requêtes Parallèles Lorsque des opérations de base de données parallèles sont véritablement nécessaires, `IDbContextFactory` fournit la solution. Cette factory crée de nouvelles instances de DbContext à la demande, chacune avec son propre état isolé. ```csharp // Program.cs builder.Services.AddDbContextFactory(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); ``` Avec la factory enregistrée, injectez-la au lieu du DbContext directement : ```csharp // DashboardService.cs public class DashboardService { private readonly IDbContextFactory _contextFactory; public DashboardService(IDbContextFactory contextFactory) { _contextFactory = contextFactory; } public async Task GetStatsAsync() { // Chaque tâche obtient sa propre instance de DbContext var ordersTask = Task.Run(async () => { await using var context = _contextFactory.CreateDbContext(); return await context.Orders.CountAsync(); }); var productsTask = Task.Run(async () => { await using var context = _contextFactory.CreateDbContext(); return await context.Products.CountAsync(); }); var customersTask = Task.Run(async () => { await using var context = _contextFactory.CreateDbContext(); return await context.Customers.CountAsync(); }); await Task.WhenAll(ordersTask, productsTask, customersTask); return new DashboardStats { Orders = ordersTask.Result, Products = productsTask.Result, Customers = customersTask.Result }; } } ``` Chaque tâche crée, utilise et dispose son propre contexte. L'instruction `await using` garantit un nettoyage approprié même si une exception se produit. ## Pooling de DbContext pour les Applications à Haut Débit La création d'un nouveau DbContext implique l'allocation de mémoire, l'initialisation du change tracker et la configuration des caches internes. Pour les applications à haut débit traitant des milliers de requêtes par seconde, cette surcharge devient mesurable. Le pooling de DbContext résout ce problème en réutilisant les instances de contexte. Lorsqu'un contexte poolé est disposé, EF Core réinitialise son état et le retourne au pool au lieu de le laisser au garbage collector. ```csharp // Program.cs builder.Services.AddDbContextPool(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default")), poolSize: 128); ``` Les [benchmarks de Dave Callan](https://davecallan.com/entity-framework-dbcontext-pooling-performance-benchmark/) montrent que le pooling réduit le temps de création du contexte de plus de 90% dans les microbenchmarks. Cependant, l'impact réel dépend des patterns de requêtes de l'application. Si les requêtes de base de données dominent le temps de traitement des requêtes, la surcharge de création du contexte est négligeable en comparaison. Le pooling introduit une contrainte : l'état du DbContext est réinitialisé lors du retour au pool. Tous les champs ou propriétés personnalisés ajoutés à la classe DbContext perdent leurs valeurs. Le change tracker est vidé, donc les modifications non commitées disparaissent. ## Combinaison du Pooling avec IDbContextFactory Pour les applications qui ont besoin à la fois des performances du pooling et du support des requêtes parallèles, utilisez `AddPooledDbContextFactory` : ```csharp // Program.cs builder.Services.AddPooledDbContextFactory(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default")), poolSize: 128); ``` Cet enregistrement fournit `IDbContextFactory` où les contextes proviennent du pool. Chaque appel à `CreateDbContext()` récupère une instance poolée, et la disposer retourne l'instance au pool. ```csharp // BatchProcessor.cs public class BatchProcessor { private readonly IDbContextFactory _contextFactory; public BatchProcessor(IDbContextFactory contextFactory) { _contextFactory = contextFactory; } public async Task ProcessBatchAsync(IEnumerable updates) { // Traitement en chunks parallèles avec des contextes poolés var chunks = updates.Chunk(100); var tasks = chunks.Select(async chunk => { await using var context = _contextFactory.CreateDbContext(); foreach (var update in chunk) { var order = await context.Orders.FindAsync(update.OrderId); if (order != null) { order.Status = update.NewStatus; } } await context.SaveChangesAsync(); }); await Task.WhenAll(tasks); } } ``` L'[analyse de Milan Jovanovic](https://milanjovanovic.tech/blog/ef-core-dbcontext-pooling) fournit des benchmarks supplémentaires montrant les performances de la factory poolée dans les scénarios de traitement par lots. ## Blazor Server : Un Cas Particulier Les applications Blazor Server nécessitent une attention particulière concernant la durée de vie de DbContext. Un circuit Blazor persiste à travers plusieurs interactions utilisateur, contrairement aux requêtes HTTP qui ont des limites claires. La durée de vie scoped par défaut devient problématique : une seule instance de DbContext vit pendant toute la durée du circuit, potentiellement des heures. Les contextes à longue durée de vie accumulent des entités trackées, augmentant l'utilisation de la mémoire et ralentissant les opérations. ```csharp // Program.cs pour Blazor Server builder.Services.AddDbContextFactory(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); ``` Dans les composants Blazor, injectez la factory et créez des contextes à courte durée de vie : ```csharp // OrderList.razor.cs public partial class OrderList : ComponentBase { [Inject] private IDbContextFactory ContextFactory { get; set; } = default!; private List _orders = new(); protected override async Task OnInitializedAsync() { await using var context = ContextFactory.CreateDbContext(); _orders = await context.Orders .AsNoTracking() .OrderByDescending(o => o.CreatedAt) .Take(50) .ToListAsync(); } } ``` L'appel `AsNoTracking()` est particulièrement important dans Blazor Server. Sans lui, chaque entité chargée reste dans le change tracker, consommant de la mémoire jusqu'à la fin du circuit. ## Services en Arrière-plan et Hosted Services Les services en arrière-plan enregistrés comme singletons ne peuvent pas injecter directement un DbContext scoped. Le service survit à tout scope, créant des erreurs de discordance de durée de vie. ```csharp // OrderProcessingService.cs public class OrderProcessingService : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; public OrderProcessingService(IServiceScopeFactory scopeFactory) { _scopeFactory = scopeFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { using var scope = _scopeFactory.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var pendingOrders = await context.Orders .Where(o => o.Status == OrderStatus.Pending) .Take(10) .ToListAsync(stoppingToken); foreach (var order in pendingOrders) { order.Status = OrderStatus.Processing; } await context.SaveChangesAsync(stoppingToken); await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); } } } ``` Le `IServiceScopeFactory` crée un nouveau scope pour chaque itération. Le scope, et le DbContext qu'il contient, est disposé à la fin de chaque itération de la boucle. Alternativement, utilisez `IDbContextFactory` pour un contrôle plus granulaire : ```csharp // OrderProcessingService.cs (version factory) public class OrderProcessingService : BackgroundService { private readonly IDbContextFactory _contextFactory; public OrderProcessingService(IDbContextFactory contextFactory) { _contextFactory = contextFactory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await using var context = _contextFactory.CreateDbContext(); var pendingOrders = await context.Orders .Where(o => o.Status == OrderStatus.Pending) .Take(10) .ToListAsync(stoppingToken); foreach (var order in pendingOrders) { order.Status = OrderStatus.Processing; } await context.SaveChangesAsync(stoppingToken); await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken); } } } ``` ## Erreurs Courantes Causant des Violations de Thread Safety Plusieurs patterns provoquent systématiquement des problèmes de thread safety de DbContext dans le code de production : **Stocker DbContext dans des champs statiques ou des singletons** : Un DbContext ne devrait jamais survivre à son scope prévu. Les références statiques rendent la même instance accessible depuis plusieurs threads. **Appels async fire-and-forget** : Démarrer une opération async sans l'attendre tout en continuant à utiliser le contexte sur le thread actuel crée un accès concurrent. ```csharp // CASSÉ : Ne pas utiliser public void UpdateAndNotify(int orderId) { var order = _context.Orders.Find(orderId); order.Status = OrderStatus.Shipped; _context.SaveChangesAsync(); // Non attendu ! _notificationService.SendAsync(order.CustomerId); // Le contexte pourrait encore être en train de sauvegarder } ``` **Injecter DbContext dans des services singleton** : Le conteneur DI lève une exception en développement, mais certaines configurations masquent cette erreur. **Utiliser DbContext à travers plusieurs awaits sans comprendre le flux d'exécution** : Chaque await est un point de suspension. Si le code repris s'exécute sur un thread différent de celui attendu, un accès concurrent peut se produire avec d'autres chemins de code. ## Guide de Décision pour la Configuration de la Durée de Vie de DbContext Le choix de la bonne configuration de DbContext dépend du type d'application et des exigences de performance : - **API web ou application MVC standard** : Utilisez `AddDbContext()` avec la durée de vie scoped par défaut. Cela couvre correctement la plupart des scénarios. - **API à haut débit (milliers de requêtes/seconde)** : Utilisez `AddDbContextPool()` pour réduire la surcharge d'allocation. - **Application nécessitant des requêtes de base de données parallèles** : Utilisez `AddDbContextFactory()` ou `AddPooledDbContextFactory()`. - **Application Blazor Server** : Utilisez `AddDbContextFactory()` avec des contextes à courte durée de vie créés par opération. - **Services en arrière-plan** : Utilisez `IServiceScopeFactory` ou `IDbContextFactory()` pour créer des contextes au sein du service. - **Traitement par lots avec parallélisme** : Utilisez `AddPooledDbContextFactory()` pour un débit optimal. Pour une couverture plus approfondie des patterns async dans ASP.NET Core, le [module de programmation async](/technologies/dotnet/interview-questions/async-aspnet-core) couvre les concepts associés. Le [module avancé EF Core](/technologies/dotnet/interview-questions/ef-core-advanced) développe l'optimisation des requêtes et le comportement du change tracking discutés ici. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/dotnet/dbcontext-lifetime-performance-thread-safety-async