# การจัดการ Lifetime ของ DbContext ใน ASP.NET Core: ประสิทธิภาพ vs ความปลอดภัยของ Thread ในการทำงานแบบ Async > เรียนรู้วิธีจัดการ lifetime ของ DbContext ใน ASP.NET Core อย่างเหมาะสม ทำความเข้าใจว่าเมื่อใดควรใช้ scoped vs transient วิธีจัดการการทำงาน async อย่างปลอดภัย และการเพิ่มประสิทธิภาพด้วย 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 --- การจัดการ lifetime ของ DbContext เป็นหนึ่งในแหล่งที่มาของ bug ที่พบบ่อยที่สุดในแอปพลิเคชัน ASP.NET Core DbContext ของ Entity Framework Core ไม่ thread-safe และการใช้งานที่ไม่ถูกต้องในการทำงานแบบ async จะนำไปสู่สถานะที่เสียหาย race condition และ exception ที่ยากต่อการจำลองซ้ำ > **DbContext ไม่ thread-safe** > > instance DbContext เดียวไม่สามารถใช้งานพร้อมกันข้ามหลาย thread ได้ หาก method async ไม่ถูก await ก่อนที่การทำงานอื่นจะเริ่มต้นบน context เดียวกัน สถานะภายในจะเสียหาย กฎนี้ใช้ได้กับทุกเวอร์ชันของ EF Core รวมถึง EF Core 9 ## ทำไม Scoped Lifetime จึงทำงานได้สำหรับ Web Request Dependency injection ของ ASP.NET Core ลงทะเบียน DbContext ด้วย scoped lifetime เป็นค่าเริ่มต้นเมื่อใช้ `AddDbContext()` แต่ละ HTTP request จะได้รับ instance DbContext ของตัวเอง และ instance นั้นจะถูก dispose เมื่อ request เสร็จสิ้น แนวทางนี้แก้ไขปัญหาสองประการ: รับประกันว่าแต่ละ request มีสถานะฐานข้อมูลที่แยกจากกัน และปรับ lifetime ของ context ให้สอดคล้องกับ unit of work pattern ที่การเปลี่ยนแปลงสะสมระหว่างการประมวลผล request และ commit พร้อมกันที่ส่วนท้าย ```csharp // Program.cs builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); ``` Scoped lifetime เหมาะสมสำหรับ request-response flow มาตรฐาน controller action หรือ Razor Page handler ทำงานบน thread เดียว และตราบใดที่ async call ทั้งหมดถูก await อย่างถูกต้อง DbContext จะยังคงอยู่ในสถานะที่สอดคล้องกันตลอดทั้ง 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); } } ``` ข้อจำกัดหลัก: ทุกการทำงาน async ต้องเสร็จสิ้นก่อนที่การทำงานถัดไปจะเริ่มต้น โค้ดด้านบนถูกต้องเพราะ `SaveChangesAsync` ถูก await ก่อนที่ method จะ return ## ปัญหา Thread Safety ในการทำงานแบบขนาน ปัญหาเกิดขึ้นเมื่อนักพัฒนาพยายามทำการทำงานฐานข้อมูลแบบขนานภายใน request เดียว พิจารณาตัวอย่างที่ผิดนี้: ```csharp // ผิด: อย่าใช้ public async Task GetDashboard() { var ordersTask = _context.Orders.CountAsync(); var productsTask = _context.Products.CountAsync(); var customersTask = _context.Customers.CountAsync(); // รัน query สามตัวบน DbContext เดียวกันพร้อมกัน await Task.WhenAll(ordersTask, productsTask, customersTask); return Ok(new { Orders = ordersTask.Result, Products = productsTask.Result, Customers = customersTask.Result }); } ``` โค้ดนี้เริ่ม async query สามตัวโดยไม่ await แต่ละตัวทีละตัว การทำงานทั้งสามทำงานพร้อมกันบน instance DbContext เดียวกัน ละเมิดข้อกำหนด thread safety ของ EF Core ผลลัพธ์ไม่สามารถคาดเดาได้: บางครั้งทำงานได้ บางครั้งโยน `InvalidOperationException` และบางครั้งส่งคืนข้อมูลที่ไม่ถูกต้อง [เอกสารอย่างเป็นทางการของ Microsoft](https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/) ระบุอย่างชัดเจนว่า method async ต้องถูก await ทันที change tracker ภายใน, connection state และ cache การคอมไพล์ query ไม่ได้ออกแบบมาสำหรับการเข้าถึงพร้อมกัน ## การใช้ IDbContextFactory สำหรับ Query แบบขนาน เมื่อต้องการการทำงานฐานข้อมูลแบบขนานอย่างแท้จริง `IDbContextFactory` ให้ทางออก factory นี้สร้าง instance DbContext ใหม่ตามต้องการ แต่ละตัวมีสถานะแยกของตัวเอง ```csharp // Program.cs builder.Services.AddDbContextFactory(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); ``` เมื่อลงทะเบียน factory แล้ว ให้ inject มันแทนที่จะ inject DbContext โดยตรง: ```csharp // DashboardService.cs public class DashboardService { private readonly IDbContextFactory _contextFactory; public DashboardService(IDbContextFactory contextFactory) { _contextFactory = contextFactory; } public async Task GetStatsAsync() { // แต่ละ task ได้รับ instance 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 }; } } ``` แต่ละ task สร้าง ใช้ และ dispose context ของตัวเอง คำสั่ง `await using` รับประกันการทำความสะอาดที่ถูกต้องแม้ว่าจะเกิด exception ## DbContext Pooling สำหรับแอปพลิเคชัน High-Throughput การสร้าง DbContext ใหม่เกี่ยวข้องกับการจัดสรรหน่วยความจำ การเริ่มต้น change tracker และการตั้งค่า cache ภายใน สำหรับแอปพลิเคชัน high-throughput ที่ประมวลผลหลายพัน request ต่อวินาที overhead นี้จะวัดได้ DbContext pooling แก้ไขปัญหานี้โดยการนำ instance context กลับมาใช้ใหม่ เมื่อ pooled context ถูก dispose EF Core จะรีเซ็ตสถานะและส่งคืนไปยัง pool แทนที่จะ garbage collect ```csharp // Program.cs builder.Services.AddDbContextPool(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default")), poolSize: 128); ``` [Benchmark โดย Dave Callan](https://davecallan.com/entity-framework-dbcontext-pooling-performance-benchmark/) แสดงให้เห็นว่า pooling ลดเวลาการสร้าง context มากกว่า 90% ใน microbenchmark อย่างไรก็ตาม ผลกระทบในโลกแห่งความเป็นจริงขึ้นอยู่กับรูปแบบ query ของแอปพลิเคชัน หาก query ฐานข้อมูลครอบงำเวลาการประมวลผล request overhead การสร้าง context จะไม่มีนัยสำคัญเมื่อเปรียบเทียบ Pooling มีข้อจำกัดหนึ่งประการ: สถานะ DbContext รีเซ็ตเมื่อกลับไปยัง pool field หรือ property ที่กำหนดเองที่เพิ่มลงในคลาส DbContext จะสูญเสียค่า change tracker ถูกล้าง ดังนั้นการเปลี่ยนแปลงที่ยังไม่ commit จะหายไป ## การรวม Pooling กับ IDbContextFactory สำหรับแอปพลิเคชันที่ต้องการทั้งประสิทธิภาพ pooling และการสนับสนุน query แบบขนาน ใช้ `AddPooledDbContextFactory`: ```csharp // Program.cs builder.Services.AddPooledDbContextFactory(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default")), poolSize: 128); ``` การลงทะเบียนนี้ให้ `IDbContextFactory` โดยที่ context มาจาก pool แต่ละการเรียก `CreateDbContext()` ดึง instance จาก pool และการ dispose จะส่งคืน instance ไปยัง pool ```csharp // BatchProcessor.cs public class BatchProcessor { private readonly IDbContextFactory _contextFactory; public BatchProcessor(IDbContextFactory contextFactory) { _contextFactory = contextFactory; } public async Task ProcessBatchAsync(IEnumerable updates) { // ประมวลผลใน chunk แบบขนานด้วย 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); } } ``` [การวิเคราะห์ของ Milan Jovanovic](https://milanjovanovic.tech/blog/ef-core-dbcontext-pooling) ให้ benchmark เพิ่มเติมที่แสดงประสิทธิภาพ pooled factory ในสถานการณ์ batch processing ## Blazor Server: กรณีพิเศษ แอปพลิเคชัน Blazor Server ต้องการความระมัดระวังเป็นพิเศษกับ lifetime ของ DbContext Blazor circuit คงอยู่ตลอดการโต้ตอบของผู้ใช้หลายครั้ง ไม่เหมือน HTTP request ที่มีขอบเขตชัดเจน Scoped lifetime เริ่มต้นกลายเป็นปัญหา: instance DbContext เดียวมีชีวิตตลอดระยะเวลา circuit ซึ่งอาจยาวหลายชั่วโมง context ที่มีอายุยาวสะสม tracked entity เพิ่มการใช้หน่วยความจำและทำให้การทำงานช้าลง ```csharp // Program.cs สำหรับ Blazor Server builder.Services.AddDbContextFactory(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); ``` ใน Blazor component inject factory และสร้าง context ระยะสั้น: ```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(); } } ``` การเรียก `AsNoTracking()` สำคัญเป็นพิเศษใน Blazor Server หากไม่มี ทุก entity ที่โหลดจะอยู่ใน change tracker ใช้หน่วยความจำจนกว่า circuit จะสิ้นสุด ## Background Service และ Hosted Service Background service ที่ลงทะเบียนเป็น singleton ไม่สามารถ inject scoped DbContext โดยตรงได้ service นี้มีอายุยาวกว่า scope ใดๆ ทำให้เกิดข้อผิดพลาด 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` สร้าง scope ใหม่สำหรับแต่ละ iteration scope และ DbContext ภายในจะถูก dispose เมื่อสิ้นสุดแต่ละ loop iteration หรือใช้ `IDbContextFactory` สำหรับการควบคุมที่ละเอียดกว่า: ```csharp // OrderProcessingService.cs (เวอร์ชัน 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); } } } ``` ## ข้อผิดพลาดทั่วไปที่ทำให้เกิดการละเมิด Thread Safety หลาย pattern ทำให้เกิดปัญหา thread safety ของ DbContext อย่างสม่ำเสมอในโค้ด production: **เก็บ DbContext ใน field static หรือ singleton**: DbContext ไม่ควรมีอายุยาวกว่า scope ที่ตั้งใจไว้ การอ้างอิง static ทำให้ instance เดียวกันสามารถเข้าถึงได้จากหลาย thread **การเรียก async แบบ fire-and-forget**: การเริ่มการทำงาน async โดยไม่ await ขณะที่ยังใช้ context บน thread ปัจจุบันต่อไปสร้างการเข้าถึงพร้อมกัน ```csharp // ผิด: อย่าใช้ public void UpdateAndNotify(int orderId) { var order = _context.Orders.Find(orderId); order.Status = OrderStatus.Shipped; _context.SaveChangesAsync(); // ไม่ await! _notificationService.SendAsync(order.CustomerId); // Context อาจยังคงบันทึกอยู่ } ``` **Inject DbContext เข้าไปใน service แบบ singleton**: container DI โยน exception ใน development แต่บางการกำหนดค่าซ่อนข้อผิดพลาดนี้ **ใช้ DbContext ข้ามหลาย await โดยไม่เข้าใจ execution flow**: แต่ละ await เป็นจุดระงับ หากโค้ดที่กลับมาทำงานบน thread ที่แตกต่างจากที่คาดไว้ การเข้าถึงพร้อมกันสามารถเกิดขึ้นกับ code path อื่น ## คู่มือการตัดสินใจสำหรับการกำหนดค่า Lifetime ของ DbContext การเลือกการกำหนดค่า DbContext ที่ถูกต้องขึ้นอยู่กับประเภทแอปพลิเคชันและข้อกำหนดด้านประสิทธิภาพ: - **แอปพลิเคชัน web API หรือ MVC มาตรฐาน**: ใช้ `AddDbContext()` กับ scoped lifetime เริ่มต้น ครอบคลุมสถานการณ์ส่วนใหญ่อย่างถูกต้อง - **API แบบ high-throughput (หลายพัน request/วินาที)**: ใช้ `AddDbContextPool()` เพื่อลด overhead การจัดสรร - **แอปพลิเคชันที่ต้องการ query ฐานข้อมูลแบบขนาน**: ใช้ `AddDbContextFactory()` หรือ `AddPooledDbContextFactory()` - **แอปพลิเคชัน Blazor Server**: ใช้ `AddDbContextFactory()` กับ context ระยะสั้นที่สร้างต่อการทำงาน - **Background service**: ใช้ `IServiceScopeFactory` หรือ `IDbContextFactory()` เพื่อสร้าง context ภายใน service - **Batch processing แบบขนาน**: ใช้ `AddPooledDbContextFactory()` สำหรับ throughput ที่เหมาะสมที่สุด สำหรับการครอบคลุมเชิงลึกเกี่ยวกับ pattern async ใน ASP.NET Core [โมดูลการเขียนโปรแกรม async](/technologies/dotnet/interview-questions/async-aspnet-core) ครอบคลุมแนวคิดที่เกี่ยวข้อง [โมดูล EF Core ขั้นสูง](/technologies/dotnet/interview-questions/ef-core-advanced) ขยายความเกี่ยวกับการเพิ่มประสิทธิภาพ query และพฤติกรรม change tracking ที่กล่าวถึงที่นี่ --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/dotnet/dbcontext-lifetime-performance-thread-safety-async