DbContext Levensduur in ASP.NET Core: Performance versus Thread Safety bij Async Operaties
DbContext levensduur optimaal beheren in ASP.NET Core: Scoped versus Transient, thread safety bij async operaties en performance-optimalisatie met DbContext pooling.

Het beheer van de DbContext-levensduur is een van de meest voorkomende bronnen van bugs in ASP.NET Core applicaties. De DbContext van Entity Framework Core is niet thread-safe, en onjuist gebruik bij async operaties leidt tot corrupte states, race conditions en exceptions die moeilijk te reproduceren zijn.
Een enkele DbContext-instantie kan niet gelijktijdig over meerdere threads worden gebruikt. Als een async methode niet wordt afgewacht voordat een andere operatie op dezelfde context begint, raakt de interne state corrupt. Deze regel geldt voor alle EF Core versies, inclusief EF Core 9.
Waarom Scoped Lifetime werkt voor Web Requests
De dependency injection van ASP.NET Core registreert DbContext standaard met een scoped levensduur bij gebruik van AddDbContext<T>(). Elke HTTP-request ontvangt zijn eigen DbContext-instantie, en die instantie wordt verwijderd wanneer de request completeert.
Deze aanpak lost twee problemen op: het zorgt ervoor dat elke request een geïsoleerde database-state heeft, en het aligneert de context-levensduur met het Unit of Work pattern waarbij wijzigingen accumuleren tijdens request-verwerking en samen committen aan het einde.
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));De scoped levensduur is geschikt voor standaard request-response flows. Een controller action of Razor Page handler draait op een enkele thread, en zolang alle async calls correct worden afgewacht, blijft de DbContext in een consistente state gedurende de hele request.
public class OrderController : ControllerBase
{
private readonly AppDbContext _context;
public OrderController(AppDbContext context)
{
_context = context;
}
[HttpPost]
public async Task<IActionResult> 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);
}
}De belangrijkste beperking: elke async operatie moet voltooid zijn voordat de volgende begint. De bovenstaande code is correct omdat SaveChangesAsync wordt afgewacht voordat de methode retourneert.
Het Thread Safety probleem bij parallelle operaties
Problemen ontstaan wanneer ontwikkelaars proberen database-operaties te paralleliseren binnen een enkele request. Bekijk dit foutieve voorbeeld:
// FOUT: Niet gebruiken
public async Task<IActionResult> GetDashboard()
{
var ordersTask = _context.Orders.CountAsync();
var productsTask = _context.Products.CountAsync();
var customersTask = _context.Customers.CountAsync();
// Drie queries op dezelfde DbContext gelijktijdig uitvoeren
await Task.WhenAll(ordersTask, productsTask, customersTask);
return Ok(new { Orders = ordersTask.Result, Products = productsTask.Result, Customers = customersTask.Result });
}Deze code start drie async queries zonder elke individueel af te wachten. Alle drie operaties draaien gelijktijdig op dezelfde DbContext-instantie, wat de thread safety vereisten van EF Core schendt. Het resultaat is onvoorspelbaar: soms werkt het, soms gooit het een InvalidOperationException, en soms retourneert het incorrecte data.
De officiële Microsoft-documentatie stelt expliciet dat async methodes direct moeten worden afgewacht. De interne change tracker, connection state en query compilation cache zijn niet ontworpen voor concurrent access.
Klaar om je .NET gesprekken te halen?
Oefen met onze interactieve simulatoren, flashcards en technische tests.
Gebruik van IDbContextFactory voor parallelle queries
Wanneer parallelle database-operaties werkelijk nodig zijn, biedt IDbContextFactory<T> de oplossing. Deze factory creëert nieuwe DbContext-instanties on demand, elk met hun eigen geïsoleerde state.
builder.Services.AddDbContextFactory<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));Met de factory geregistreerd, wordt deze geïnjecteerd in plaats van de DbContext direct:
public class DashboardService
{
private readonly IDbContextFactory<AppDbContext> _contextFactory;
public DashboardService(IDbContextFactory<AppDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task<DashboardStats> GetStatsAsync()
{
// Elke task krijgt zijn eigen DbContext-instantie
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
};
}
}Elke task creëert, gebruikt en disposed zijn eigen context. Het await using statement zorgt voor correcte cleanup zelfs als er een exception optreedt.
DbContext Pooling voor High-Throughput Applicaties
Het aanmaken van een nieuwe DbContext omvat geheugenallocatie, initialisatie van de change tracker en opzetten van interne caches. Voor high-throughput applicaties die duizenden requests per seconde verwerken, wordt deze overhead meetbaar.
DbContext pooling adresseert dit door context-instanties te hergebruiken. Wanneer een gepoolde context wordt gedisposed, reset EF Core zijn state en retourneert hem naar de pool in plaats van garbage collection.
builder.Services.AddDbContextPool<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")),
poolSize: 128);Benchmarks van Dave Callan tonen aan dat pooling de context-aanmaaktijd met meer dan 90% vermindert in microbenchmarks. Echter, de real-world impact hangt af van de query-patronen van de applicatie. Als database-queries de request-verwerkingstijd domineren, is de context-aanmaak overhead verwaarloosbaar in vergelijking.
Pooling introduceert één beperking: de DbContext state reset bij terugkeer naar de pool. Alle aangepaste velden of properties toegevoegd aan de DbContext class verliezen hun waarden. De change tracker wordt geleegd, dus uncommitted wijzigingen verdwijnen.
Combineren van Pooling met IDbContextFactory
Voor applicaties die zowel pooling-performance als ondersteuning voor parallelle queries nodig hebben, is AddPooledDbContextFactory beschikbaar:
builder.Services.AddPooledDbContextFactory<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")),
poolSize: 128);Deze registratie biedt IDbContextFactory<AppDbContext> waarbij contexts uit de pool komen. Elke CreateDbContext() aanroep haalt een gepoolde instantie op, en disposing retourneert de instantie naar de pool.
public class BatchProcessor
{
private readonly IDbContextFactory<AppDbContext> _contextFactory;
public BatchProcessor(IDbContextFactory<AppDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task ProcessBatchAsync(IEnumerable<OrderUpdate> updates)
{
// Parallelle verwerking in chunks met gepoolde contexts
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);
}
}Milan Jovanovic's analyse biedt aanvullende benchmarks die gepoolde factory performance tonen in batch-verwerkingsscenario's.
Blazor Server: Een speciaal geval
Blazor Server applicaties vereisen extra zorg met DbContext levensduur. Een Blazor circuit persisteert over meerdere gebruikersinteracties, in tegenstelling tot HTTP-requests die duidelijke grenzen hebben.
De standaard scoped levensduur wordt problematisch: een enkele DbContext-instantie leeft voor de gehele circuit-duur, potentieel uren. Langlevende contexts accumuleren tracked entities, verhogen geheugengebruik en vertragen operaties.
builder.Services.AddDbContextFactory<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));In Blazor componenten wordt de factory geïnjecteerd en worden kortlevende contexts gecreëerd:
public partial class OrderList : ComponentBase
{
[Inject]
private IDbContextFactory<AppDbContext> ContextFactory { get; set; } = default!;
private List<Order> _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();
}
}De AsNoTracking() aanroep is bijzonder belangrijk in Blazor Server. Zonder dit blijft elke geladen entity in de change tracker, wat geheugen verbruikt tot het einde van het circuit.
Background Services en Hosted Services
Background services geregistreerd als singletons kunnen geen scoped DbContext direct injecteren. De service overleeft elke scope, wat levensduur-mismatch fouten veroorzaakt.
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<AppDbContext>();
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);
}
}
}De IServiceScopeFactory creëert een nieuwe scope voor elke iteratie. De scope, en de DbContext erin, wordt gedisposed aan het einde van elke loop-iteratie.
Alternatief kan IDbContextFactory worden gebruikt voor meer granulaire controle:
public class OrderProcessingService : BackgroundService
{
private readonly IDbContextFactory<AppDbContext> _contextFactory;
public OrderProcessingService(IDbContextFactory<AppDbContext> 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);
}
}
}Veelvoorkomende fouten die Thread Safety schendingen veroorzaken
Verschillende patronen veroorzaken consistent DbContext thread safety problemen in productiecode:
Opslaan van DbContext in statische velden of singletons: Een DbContext mag nooit zijn beoogde scope overleven. Statische referenties maken dezelfde instantie toegankelijk vanaf meerdere threads.
Fire-and-forget async calls: Het starten van een async operatie zonder af te wachten terwijl de context op de huidige thread wordt gebruikt, creëert concurrent access.
// FOUT: Niet gebruiken
public void UpdateAndNotify(int orderId)
{
var order = _context.Orders.Find(orderId);
order.Status = OrderStatus.Shipped;
_context.SaveChangesAsync(); // Niet afgewacht!
_notificationService.SendAsync(order.CustomerId); // Context is mogelijk nog aan het opslaan
}Injecteren van DbContext in singleton services: De DI container gooit een exception in development, maar sommige configuraties maskeren deze fout.
Gebruik van DbContext over meerdere awaits zonder begrip van de executie-flow: Elke await is een opschortingspunt. Als de hervatte code op een andere thread draait dan verwacht, kan concurrent access optreden met andere code-paden.
Begin met oefenen!
Test je kennis met onze gespreksimulatoren en technische tests.
Beslissingsgids voor DbContext Levensduur Configuratie
Het kiezen van de juiste DbContext configuratie hangt af van het applicatie-type en performance-vereisten:
- Standaard web API of MVC applicatie: Gebruik
AddDbContext<T>()met standaard scoped levensduur. Dit dekt de meeste scenario's correct af. - High-throughput API (duizenden requests/seconde): Gebruik
AddDbContextPool<T>()om allocatie-overhead te verminderen. - Applicatie die parallelle database queries vereist: Gebruik
AddDbContextFactory<T>()ofAddPooledDbContextFactory<T>(). - Blazor Server applicatie: Gebruik
AddDbContextFactory<T>()met kortlevende contexts gecreëerd per operatie. - Background services: Gebruik
IServiceScopeFactoryofIDbContextFactory<T>()om contexts binnen de service te creëren. - Batch-verwerking met parallellisme: Gebruik
AddPooledDbContextFactory<T>()voor optimale throughput.
Voor diepere behandeling van async patronen in ASP.NET Core behandelt de async programmering module gerelateerde concepten. De EF Core geavanceerde module breidt uit op query-optimalisatie en change tracking gedrag dat hier is besproken.
Zie jij de bug in .NET?
Een echt codefragment, een verborgen bug, één poging per dag. Zonder account uit te proberen.

Geschreven door
Anthony Fillion-MailletOprichter van SharpSkill
Al meer dan 10 jaar fullstack-ontwikkelaar. Hij leidt SharpSkill en staat in voor alles wat hier verschijnt.
Bijgewerkt op 7 september 2026
Tags
Delen
Gerelateerde artikelen

.NET 10 in 2026: Nieuwe Features, Native AOT en C# 14 voor Sollicitatiepreparatie
.NET 10 verschijnt als Long-Term Support release met Native AOT-verbeteringen, C# 14 extension members, de field keyword en file-based apps. Een complete gids over nieuwe features, prestatiewinst en interview-ready kennis voor .NET-ontwikkelaars in 2026.

Geavanceerd C# LINQ in 2026: Operators, Performance en Sollicitatievragen
Een diepgaande gids over geavanceerde LINQ-operators, deferred execution, performance-optimalisatie en veelgestelde C# sollicitatievragen voor .NET-ontwikkelaars.

.NET 9 Blazor: Full-Stack Development met Blazor United in 2026
Blazor United in .NET 9 combineert statische SSR, Server- en WebAssembly-rendermodi in één full-stack architectuur. Een praktische tutorial over rendermodi, streaming rendering, dependency injection en productiepatronen.