การจัดการ Lifetime ของ DbContext ใน ASP.NET Core: ประสิทธิภาพ vs ความปลอดภัยของ Thread ในการทำงานแบบ Async

เรียนรู้วิธีจัดการ lifetime ของ DbContext ใน ASP.NET Core อย่างเหมาะสม ทำความเข้าใจว่าเมื่อใดควรใช้ scoped vs transient วิธีจัดการการทำงาน async อย่างปลอดภัย และการเพิ่มประสิทธิภาพด้วย DbContext pooling

รูปแบบ lifetime ของ DbContext และความปลอดภัยของ thread ในการทำงานแบบ async ของ ASP.NET Core

การจัดการ 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<T>() แต่ละ HTTP request จะได้รับ instance DbContext ของตัวเอง และ instance นั้นจะถูก dispose เมื่อ request เสร็จสิ้น

แนวทางนี้แก้ไขปัญหาสองประการ: รับประกันว่าแต่ละ request มีสถานะฐานข้อมูลที่แยกจากกัน และปรับ lifetime ของ context ให้สอดคล้องกับ unit of work pattern ที่การเปลี่ยนแปลงสะสมระหว่างการประมวลผล request และ commit พร้อมกันที่ส่วนท้าย

Program.cscsharp
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

Scoped lifetime เหมาะสมสำหรับ request-response flow มาตรฐาน controller action หรือ Razor Page handler ทำงานบน thread เดียว และตราบใดที่ async call ทั้งหมดถูก await อย่างถูกต้อง DbContext จะยังคงอยู่ในสถานะที่สอดคล้องกันตลอดทั้ง request

OrderController.cscsharp
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);
    }
}

ข้อจำกัดหลัก: ทุกการทำงาน async ต้องเสร็จสิ้นก่อนที่การทำงานถัดไปจะเริ่มต้น โค้ดด้านบนถูกต้องเพราะ SaveChangesAsync ถูก await ก่อนที่ method จะ return

ปัญหา Thread Safety ในการทำงานแบบขนาน

ปัญหาเกิดขึ้นเมื่อนักพัฒนาพยายามทำการทำงานฐานข้อมูลแบบขนานภายใน request เดียว พิจารณาตัวอย่างที่ผิดนี้:

csharp
// ผิด: อย่าใช้
public async Task<IActionResult> 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 ระบุอย่างชัดเจนว่า method async ต้องถูก await ทันที change tracker ภายใน, connection state และ cache การคอมไพล์ query ไม่ได้ออกแบบมาสำหรับการเข้าถึงพร้อมกัน

พร้อมที่จะพิชิตการสัมภาษณ์ .NET แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การใช้ IDbContextFactory สำหรับ Query แบบขนาน

เมื่อต้องการการทำงานฐานข้อมูลแบบขนานอย่างแท้จริง IDbContextFactory<T> ให้ทางออก factory นี้สร้าง instance DbContext ใหม่ตามต้องการ แต่ละตัวมีสถานะแยกของตัวเอง

Program.cscsharp
builder.Services.AddDbContextFactory<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

เมื่อลงทะเบียน factory แล้ว ให้ inject มันแทนที่จะ inject DbContext โดยตรง:

DashboardService.cscsharp
public class DashboardService
{
    private readonly IDbContextFactory<AppDbContext> _contextFactory;

    public DashboardService(IDbContextFactory<AppDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public async Task<DashboardStats> 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

Program.cscsharp
builder.Services.AddDbContextPool<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")),
    poolSize: 128);

Benchmark โดย Dave Callan แสดงให้เห็นว่า pooling ลดเวลาการสร้าง context มากกว่า 90% ใน microbenchmark อย่างไรก็ตาม ผลกระทบในโลกแห่งความเป็นจริงขึ้นอยู่กับรูปแบบ query ของแอปพลิเคชัน หาก query ฐานข้อมูลครอบงำเวลาการประมวลผล request overhead การสร้าง context จะไม่มีนัยสำคัญเมื่อเปรียบเทียบ

Pooling มีข้อจำกัดหนึ่งประการ: สถานะ DbContext รีเซ็ตเมื่อกลับไปยัง pool field หรือ property ที่กำหนดเองที่เพิ่มลงในคลาส DbContext จะสูญเสียค่า change tracker ถูกล้าง ดังนั้นการเปลี่ยนแปลงที่ยังไม่ commit จะหายไป

การรวม Pooling กับ IDbContextFactory

สำหรับแอปพลิเคชันที่ต้องการทั้งประสิทธิภาพ pooling และการสนับสนุน query แบบขนาน ใช้ AddPooledDbContextFactory:

Program.cscsharp
builder.Services.AddPooledDbContextFactory<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")),
    poolSize: 128);

การลงทะเบียนนี้ให้ IDbContextFactory<AppDbContext> โดยที่ context มาจาก pool แต่ละการเรียก CreateDbContext() ดึง instance จาก pool และการ dispose จะส่งคืน instance ไปยัง pool

BatchProcessor.cscsharp
public class BatchProcessor
{
    private readonly IDbContextFactory<AppDbContext> _contextFactory;

    public BatchProcessor(IDbContextFactory<AppDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public async Task ProcessBatchAsync(IEnumerable<OrderUpdate> 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 ให้ benchmark เพิ่มเติมที่แสดงประสิทธิภาพ pooled factory ในสถานการณ์ batch processing

Blazor Server: กรณีพิเศษ

แอปพลิเคชัน Blazor Server ต้องการความระมัดระวังเป็นพิเศษกับ lifetime ของ DbContext Blazor circuit คงอยู่ตลอดการโต้ตอบของผู้ใช้หลายครั้ง ไม่เหมือน HTTP request ที่มีขอบเขตชัดเจน

Scoped lifetime เริ่มต้นกลายเป็นปัญหา: instance DbContext เดียวมีชีวิตตลอดระยะเวลา circuit ซึ่งอาจยาวหลายชั่วโมง context ที่มีอายุยาวสะสม tracked entity เพิ่มการใช้หน่วยความจำและทำให้การทำงานช้าลง

Program.cs สำหรับ Blazor Servercsharp
builder.Services.AddDbContextFactory<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

ใน Blazor component inject factory และสร้าง context ระยะสั้น:

OrderList.razor.cscsharp
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();
    }
}

การเรียก AsNoTracking() สำคัญเป็นพิเศษใน Blazor Server หากไม่มี ทุก entity ที่โหลดจะอยู่ใน change tracker ใช้หน่วยความจำจนกว่า circuit จะสิ้นสุด

Background Service และ Hosted Service

Background service ที่ลงทะเบียนเป็น singleton ไม่สามารถ inject scoped DbContext โดยตรงได้ service นี้มีอายุยาวกว่า scope ใดๆ ทำให้เกิดข้อผิดพลาด lifetime ไม่ตรงกัน

OrderProcessingService.cscsharp
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);
        }
    }
}

IServiceScopeFactory สร้าง scope ใหม่สำหรับแต่ละ iteration scope และ DbContext ภายในจะถูก dispose เมื่อสิ้นสุดแต่ละ loop iteration

หรือใช้ IDbContextFactory สำหรับการควบคุมที่ละเอียดกว่า:

OrderProcessingService.cs (เวอร์ชัน factory)csharp
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);
        }
    }
}

ข้อผิดพลาดทั่วไปที่ทำให้เกิดการละเมิด 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<T>() กับ scoped lifetime เริ่มต้น ครอบคลุมสถานการณ์ส่วนใหญ่อย่างถูกต้อง
  • API แบบ high-throughput (หลายพัน request/วินาที): ใช้ AddDbContextPool<T>() เพื่อลด overhead การจัดสรร
  • แอปพลิเคชันที่ต้องการ query ฐานข้อมูลแบบขนาน: ใช้ AddDbContextFactory<T>() หรือ AddPooledDbContextFactory<T>()
  • แอปพลิเคชัน Blazor Server: ใช้ AddDbContextFactory<T>() กับ context ระยะสั้นที่สร้างต่อการทำงาน
  • Background service: ใช้ IServiceScopeFactory หรือ IDbContextFactory<T>() เพื่อสร้าง context ภายใน service
  • Batch processing แบบขนาน: ใช้ AddPooledDbContextFactory<T>() สำหรับ throughput ที่เหมาะสมที่สุด

สำหรับการครอบคลุมเชิงลึกเกี่ยวกับ pattern async ใน ASP.NET Core โมดูลการเขียนโปรแกรม async ครอบคลุมแนวคิดที่เกี่ยวข้อง โมดูล EF Core ขั้นสูง ขยายความเกี่ยวกับการเพิ่มประสิทธิภาพ query และพฤติกรรม change tracking ที่กล่าวถึงที่นี่

ชาเลนจ์ประจำวัน

คุณหาบั๊กใน .NET เจอไหม

โค้ดจริงหนึ่งชิ้น บั๊กที่ซ่อนอยู่หนึ่งจุด วันละหนึ่งครั้ง ลองได้โดยไม่ต้องมีบัญชี

Anthony Fillion-Maillet

เขียนโดย

Anthony Fillion-Maillet

ผู้ก่อตั้ง SharpSkill

เป็นนักพัฒนาฟูลสแตกมากว่า 10 ปี ดูแล SharpSkill และรับผิดชอบทุกสิ่งที่เผยแพร่ที่นี่

อัปเดตเมื่อ 7 กันยายน 2569

แท็ก

#dotnet
#entity-framework
#aspnet-core
#async
#performance

แชร์

บทความที่เกี่ยวข้อง

Entity Framework Core Performance Optimization

Entity Framework Core: การเพิ่มประสิทธิภาพและแนวทางปฏิบัติที่ดีที่สุดในปี 2026

เชี่ยวชาญการเพิ่มประสิทธิภาพ EF Core 10 ด้วย AsNoTracking, compiled queries, split queries, batch operations และตัวดำเนินการ LeftJoin ใหม่ ตัวอย่าง C# ที่ใช้งานได้จริงสำหรับแอปพลิเคชัน .NET 10 ใน production

.NET 10 new features Native AOT C# 14

.NET 10 ในปี 2026: ฟีเจอร์ใหม่ Native AOT และ C# 14 สำหรับเตรียมสัมภาษณ์งาน

.NET 10 เปิดตัวในฐานะรีลีส long-term support พร้อมการปรับปรุง Native AOT, extension member C# 14, field keyword และ file-based apps คู่มือครบถ้วนครอบคลุมฟีเจอร์ใหม่ การปรับปรุงประสิทธิภาพ และความรู้สำหรับสัมภาษณ์งานสำหรับนักพัฒนา .NET ในปี 2026

การพัฒนาเว็บแอปพลิเคชัน full-stack ด้วย .NET 9 Blazor United และโหมดการ render หลากหลาย

.NET 9 Blazor: พัฒนาแอปพลิเคชัน Full-Stack ด้วย Blazor United ปี 2026

Blazor United ใน .NET 9 ผสานรวม Static SSR, Interactive Server และ WebAssembly ไว้ในสถาปัตยกรรมเดียว บทความนี้อธิบายโหมดการ render ทั้ง 4 แบบ, streaming rendering, dependency injection, AOT compilation และแนวทางปฏิบัติสำหรับ production อย่างครบถ้วน