# Quản lý Vòng đời DbContext trong ASP.NET Core: Hiệu năng vs An toàn Thread trong Thao tác Async > Tìm hiểu cách quản lý vòng đời DbContext trong ASP.NET Core một cách tối ưu. Hiểu khi nào sử dụng scoped vs transient, cách xử lý thao tác async an toàn, và tối ưu hiệu năng với DbContext pooling. - Published: 2026-09-07 - Updated: 2026-09-07 - Author: Anthony Fillion-Maillet - Tags: dotnet, entity-framework, aspnet-core, async, performance - Reading time: 9 min --- Quản lý vòng đời DbContext là một trong những nguồn bug phổ biến nhất trong các ứng dụng ASP.NET Core. DbContext của Entity Framework Core không thread-safe, và việc sử dụng sai trong các thao tác async dẫn đến state bị hỏng, race condition, và các exception khó tái hiện. > **DbContext không thread-safe** > > Một instance DbContext duy nhất không thể được sử dụng đồng thời trên nhiều thread. Nếu một method async không được await trước khi thao tác khác bắt đầu trên cùng context, state nội bộ sẽ bị hỏng. Quy tắc này áp dụng cho tất cả các phiên bản EF Core, bao gồm EF Core 9. ## Tại sao Scoped Lifetime Hoạt động cho Web Request Dependency injection của ASP.NET Core đăng ký DbContext với scoped lifetime theo mặc định khi sử dụng `AddDbContext()`. Mỗi HTTP request nhận được instance DbContext riêng, và instance đó được dispose khi request hoàn thành. Cách tiếp cận này giải quyết hai vấn đề: đảm bảo mỗi request có state database cô lập, và căn chỉnh vòng đời context với pattern unit of work nơi các thay đổi tích lũy trong quá trình xử lý request và commit cùng nhau ở cuối. ```csharp // Program.cs builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); ``` Scoped lifetime phù hợp cho các luồng request-response tiêu chuẩn. Một controller action hoặc Razor Page handler chạy trên single thread, và miễn là tất cả các async call được await đúng cách, DbContext vẫn ở trạng thái nhất quán trong suốt request. ```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); } } ``` Điều kiện tiên quyết: mọi thao tác async phải hoàn thành trước khi thao tác tiếp theo bắt đầu. Code ở trên đúng vì `SaveChangesAsync` được await trước khi method return. ## Vấn đề Thread Safety trong Thao tác Song song Vấn đề xuất hiện khi developer cố gắng song song hóa các thao tác database trong một request. Xem xét ví dụ sai này: ```csharp // SAI: Không sử dụng public async Task GetDashboard() { var ordersTask = _context.Orders.CountAsync(); var productsTask = _context.Products.CountAsync(); var customersTask = _context.Customers.CountAsync(); // Chạy ba query trên cùng một DbContext đồng thời await Task.WhenAll(ordersTask, productsTask, customersTask); return Ok(new { Orders = ordersTask.Result, Products = productsTask.Result, Customers = customersTask.Result }); } ``` Code này bắt đầu ba async query mà không await từng cái riêng lẻ. Cả ba thao tác chạy đồng thời trên cùng một instance DbContext, vi phạm yêu cầu thread safety của EF Core. Kết quả không thể đoán trước: đôi khi hoạt động, đôi khi ném `InvalidOperationException`, và đôi khi trả về dữ liệu sai. [Tài liệu chính thức của Microsoft](https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/) nêu rõ rằng các method async phải được await ngay lập tức. Change tracker nội bộ, connection state, và cache biên dịch query không được thiết kế cho truy cập đồng thời. ## Sử dụng IDbContextFactory cho Query Song song Khi thực sự cần thao tác database song song, `IDbContextFactory` cung cấp giải pháp. Factory này tạo instance DbContext mới theo yêu cầu, mỗi cái có state cô lập riêng. ```csharp // Program.cs builder.Services.AddDbContextFactory(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); ``` Với factory đã đăng ký, inject nó thay vì DbContext trực tiếp: ```csharp // DashboardService.cs public class DashboardService { private readonly IDbContextFactory _contextFactory; public DashboardService(IDbContextFactory contextFactory) { _contextFactory = contextFactory; } public async Task GetStatsAsync() { // Mỗi task nhận được instance DbContext riêng 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 }; } } ``` Mỗi task tạo, sử dụng, và dispose context riêng. Câu lệnh `await using` đảm bảo dọn dẹp đúng cách ngay cả khi exception xảy ra. ## DbContext Pooling cho Ứng dụng High-Throughput Việc tạo DbContext mới liên quan đến cấp phát bộ nhớ, khởi tạo change tracker, và thiết lập cache nội bộ. Đối với ứng dụng high-throughput xử lý hàng nghìn request mỗi giây, overhead này trở nên đáng kể. DbContext pooling giải quyết vấn đề này bằng cách tái sử dụng các instance context. Khi pooled context được dispose, EF Core reset state của nó và trả về pool thay vì garbage collect. ```csharp // Program.cs builder.Services.AddDbContextPool(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default")), poolSize: 128); ``` [Benchmark của Dave Callan](https://davecallan.com/entity-framework-dbcontext-pooling-performance-benchmark/) cho thấy pooling giảm thời gian tạo context hơn 90% trong microbenchmark. Tuy nhiên, tác động thực tế phụ thuộc vào pattern query của ứng dụng. Nếu database query chi phối thời gian xử lý request, overhead tạo context trở nên không đáng kể khi so sánh. Pooling đưa ra một ràng buộc: state DbContext được reset khi trả về pool. Bất kỳ field hoặc property tùy chỉnh nào thêm vào class DbContext sẽ mất giá trị. Change tracker được xóa, vì vậy các thay đổi chưa commit sẽ biến mất. ## Kết hợp Pooling với IDbContextFactory Đối với ứng dụng cần cả hiệu năng pooling và hỗ trợ query song song, sử dụng `AddPooledDbContextFactory`: ```csharp // Program.cs builder.Services.AddPooledDbContextFactory(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default")), poolSize: 128); ``` Đăng ký này cung cấp `IDbContextFactory` nơi context đến từ pool. Mỗi lệnh gọi `CreateDbContext()` lấy instance từ pool, và dispose nó trả instance về pool. ```csharp // BatchProcessor.cs public class BatchProcessor { private readonly IDbContextFactory _contextFactory; public BatchProcessor(IDbContextFactory contextFactory) { _contextFactory = contextFactory; } public async Task ProcessBatchAsync(IEnumerable updates) { // Xử lý theo chunk song song với pooled context 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); } } ``` [Phân tích của Milan Jovanovic](https://milanjovanovic.tech/blog/ef-core-dbcontext-pooling) cung cấp benchmark bổ sung cho thấy hiệu năng pooled factory trong các kịch bản batch processing. ## Blazor Server: Trường hợp Đặc biệt Ứng dụng Blazor Server yêu cầu sự chú ý đặc biệt với vòng đời DbContext. Một Blazor circuit tồn tại qua nhiều tương tác người dùng, không giống HTTP request có ranh giới rõ ràng. Scoped lifetime mặc định trở nên có vấn đề: một instance DbContext duy nhất sống trong toàn bộ thời gian circuit, có thể kéo dài hàng giờ. Context sống lâu tích lũy tracked entity, tăng sử dụng bộ nhớ và làm chậm các thao tác. ```csharp // Program.cs cho Blazor Server builder.Services.AddDbContextFactory(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); ``` Trong component Blazor, inject factory và tạo context ngắn hạn: ```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ệnh gọi `AsNoTracking()` đặc biệt quan trọng trong Blazor Server. Không có nó, mọi entity được load sẽ ở lại trong change tracker, tiêu thụ bộ nhớ cho đến khi circuit kết thúc. ## Background Service và Hosted Service Background service đăng ký dưới dạng singleton không thể inject scoped DbContext trực tiếp. Service này sống lâu hơn bất kỳ scope nào, tạo ra lỗi không khớp lifetime. ```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); } } } ``` `IServiceScopeFactory` tạo scope mới cho mỗi vòng lặp. Scope, và DbContext bên trong nó, được dispose ở cuối mỗi vòng lặp. Ngoài ra, sử dụng `IDbContextFactory` để kiểm soát chi tiết hơn: ```csharp // OrderProcessingService.cs (phiên bản 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); } } } ``` ## Lỗi Thường gặp Gây ra Vi phạm Thread Safety Một số pattern liên tục gây ra vấn đề thread safety DbContext trong code production: **Lưu DbContext trong field static hoặc singleton**: DbContext không bao giờ nên sống lâu hơn scope dự định. Tham chiếu static làm cho cùng một instance có thể truy cập từ nhiều thread. **Gọi async fire-and-forget**: Bắt đầu thao tác async mà không await trong khi tiếp tục sử dụng context trên thread hiện tại tạo ra truy cập đồng thời. ```csharp // SAI: Không sử dụng public void UpdateAndNotify(int orderId) { var order = _context.Orders.Find(orderId); order.Status = OrderStatus.Shipped; _context.SaveChangesAsync(); // Không await! _notificationService.SendAsync(order.CustomerId); // Context có thể vẫn đang lưu } ``` **Inject DbContext vào service singleton**: Container DI ném exception trong development, nhưng một số cấu hình che giấu lỗi này. **Sử dụng DbContext qua nhiều await mà không hiểu luồng thực thi**: Mỗi await là điểm tạm dừng. Nếu code tiếp tục chạy trên thread khác với dự kiến, truy cập đồng thời có thể xảy ra với các đường dẫn code khác. ## Hướng dẫn Quyết định cho Cấu hình Lifetime DbContext Việc chọn cấu hình DbContext phù hợp phụ thuộc vào loại ứng dụng và yêu cầu hiệu năng: - **Ứng dụng web API hoặc MVC tiêu chuẩn**: Sử dụng `AddDbContext()` với scoped lifetime mặc định. Điều này bao phủ hầu hết các kịch bản đúng cách. - **API high-throughput (hàng nghìn request/giây)**: Sử dụng `AddDbContextPool()` để giảm overhead cấp phát. - **Ứng dụng yêu cầu query database song song**: Sử dụng `AddDbContextFactory()` hoặc `AddPooledDbContextFactory()`. - **Ứng dụng Blazor Server**: Sử dụng `AddDbContextFactory()` với context ngắn hạn được tạo cho mỗi thao tác. - **Background service**: Sử dụng `IServiceScopeFactory` hoặc `IDbContextFactory()` để tạo context trong service. - **Batch processing với song song hóa**: Sử dụng `AddPooledDbContextFactory()` cho throughput tối ưu. Để tìm hiểu sâu hơn về các pattern async trong ASP.NET Core, [module lập trình async](/technologies/dotnet/interview-questions/async-aspnet-core) bao gồm các khái niệm liên quan. [Module EF Core nâng cao](/technologies/dotnet/interview-questions/ef-core-advanced) mở rộng về tối ưu query và hành vi change tracking được thảo luận ở đây. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/dotnet/dbcontext-lifetime-performance-thread-safety-async