# Manajemen Lifetime DbContext di ASP.NET Core: Performa vs Keamanan Thread pada Operasi Async > Pelajari cara mengelola lifetime DbContext di ASP.NET Core secara optimal. Pahami kapan menggunakan scoped vs transient, cara menangani operasi async dengan aman, dan optimasi performa dengan 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 --- Manajemen lifetime DbContext merupakan salah satu sumber bug paling umum dalam aplikasi ASP.NET Core. DbContext dari Entity Framework Core tidak thread-safe, dan penggunaan yang salah pada operasi async dapat menyebabkan state yang rusak, race condition, dan exception yang sulit direproduksi. > **DbContext tidak thread-safe** > > Satu instance DbContext tidak dapat digunakan secara bersamaan di beberapa thread. Jika sebuah method async tidak di-await sebelum operasi lain dimulai pada context yang sama, state internal akan rusak. Aturan ini berlaku untuk semua versi EF Core, termasuk EF Core 9. ## Mengapa Scoped Lifetime Bekerja untuk Web Request Dependency injection ASP.NET Core mendaftarkan DbContext dengan scoped lifetime secara default saat menggunakan `AddDbContext()`. Setiap HTTP request menerima instance DbContext sendiri, dan instance tersebut akan di-dispose saat request selesai. Pendekatan ini menyelesaikan dua masalah: memastikan setiap request memiliki state database yang terisolasi, dan menyelaraskan lifetime context dengan pola unit of work di mana perubahan terakumulasi selama pemrosesan request dan di-commit bersama di akhir. ```csharp // Program.cs builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); ``` Scoped lifetime cocok untuk alur request-response standar. Controller action atau Razor Page handler berjalan pada single thread, dan selama semua async call di-await dengan benar, DbContext tetap dalam state yang konsisten sepanjang 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); } } ``` Kunci utamanya: setiap operasi async harus selesai sebelum yang berikutnya dimulai. Kode di atas benar karena `SaveChangesAsync` di-await sebelum method return. ## Masalah Thread Safety pada Operasi Paralel Masalah muncul ketika developer mencoba memparalelkan operasi database dalam satu request. Perhatikan contoh yang salah ini: ```csharp // SALAH: Jangan gunakan public async Task GetDashboard() { var ordersTask = _context.Orders.CountAsync(); var productsTask = _context.Products.CountAsync(); var customersTask = _context.Customers.CountAsync(); // Menjalankan tiga query pada DbContext yang sama secara bersamaan await Task.WhenAll(ordersTask, productsTask, customersTask); return Ok(new { Orders = ordersTask.Result, Products = productsTask.Result, Customers = customersTask.Result }); } ``` Kode ini memulai tiga async query tanpa meng-await masing-masing secara individual. Ketiga operasi berjalan secara bersamaan pada instance DbContext yang sama, melanggar persyaratan thread safety EF Core. Hasilnya tidak terprediksi: terkadang berhasil, terkadang melempar `InvalidOperationException`, dan terkadang mengembalikan data yang salah. [Dokumentasi resmi Microsoft](https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/) secara eksplisit menyatakan bahwa method async harus segera di-await. Change tracker internal, connection state, dan cache kompilasi query tidak dirancang untuk akses bersamaan. ## Menggunakan IDbContextFactory untuk Query Paralel Ketika operasi database paralel benar-benar diperlukan, `IDbContextFactory` menyediakan solusinya. Factory ini membuat instance DbContext baru sesuai permintaan, masing-masing dengan state terisolasi sendiri. ```csharp // Program.cs builder.Services.AddDbContextFactory(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); ``` Dengan factory terdaftar, inject factory tersebut alih-alih DbContext secara langsung: ```csharp // DashboardService.cs public class DashboardService { private readonly IDbContextFactory _contextFactory; public DashboardService(IDbContextFactory contextFactory) { _contextFactory = contextFactory; } public async Task GetStatsAsync() { // Setiap task mendapat instance DbContext sendiri 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 }; } } ``` Setiap task membuat, menggunakan, dan men-dispose context sendiri. Statement `await using` memastikan pembersihan yang tepat bahkan jika exception terjadi. ## DbContext Pooling untuk Aplikasi High-Throughput Membuat DbContext baru melibatkan alokasi memori, inisialisasi change tracker, dan pengaturan cache internal. Untuk aplikasi high-throughput yang memproses ribuan request per detik, overhead ini menjadi terukur. DbContext pooling mengatasi ini dengan menggunakan kembali instance context. Ketika pooled context di-dispose, EF Core mereset state-nya dan mengembalikannya ke pool alih-alih mengumpulkannya untuk garbage collection. ```csharp // Program.cs builder.Services.AddDbContextPool(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default")), poolSize: 128); ``` [Benchmark oleh Dave Callan](https://davecallan.com/entity-framework-dbcontext-pooling-performance-benchmark/) menunjukkan pooling mengurangi waktu pembuatan context lebih dari 90% dalam microbenchmark. Namun, dampak dunia nyata tergantung pada pola query aplikasi. Jika query database mendominasi waktu pemrosesan request, overhead pembuatan context menjadi tidak signifikan sebagai perbandingan. Pooling memperkenalkan satu batasan: state DbContext direset saat kembali ke pool. Field atau properti kustom yang ditambahkan ke kelas DbContext akan kehilangan nilainya. Change tracker dibersihkan, sehingga perubahan yang belum di-commit akan hilang. ## Menggabungkan Pooling dengan IDbContextFactory Untuk aplikasi yang membutuhkan performa pooling dan dukungan query paralel, gunakan `AddPooledDbContextFactory`: ```csharp // Program.cs builder.Services.AddPooledDbContextFactory(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default")), poolSize: 128); ``` Registrasi ini menyediakan `IDbContextFactory` di mana context berasal dari pool. Setiap panggilan `CreateDbContext()` mengambil instance dari pool, dan men-dispose-nya mengembalikan instance ke pool. ```csharp // BatchProcessor.cs public class BatchProcessor { private readonly IDbContextFactory _contextFactory; public BatchProcessor(IDbContextFactory contextFactory) { _contextFactory = contextFactory; } public async Task ProcessBatchAsync(IEnumerable updates) { // Proses dalam chunk paralel dengan 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); } } ``` [Analisis Milan Jovanovic](https://milanjovanovic.tech/blog/ef-core-dbcontext-pooling) menyediakan benchmark tambahan yang menunjukkan performa pooled factory dalam skenario batch processing. ## Blazor Server: Kasus Khusus Aplikasi Blazor Server memerlukan perhatian ekstra dengan lifetime DbContext. Sebuah circuit Blazor bertahan di beberapa interaksi pengguna, tidak seperti HTTP request yang memiliki batasan jelas. Scoped lifetime default menjadi bermasalah: satu instance DbContext hidup selama durasi circuit, berpotensi berjam-jam. Context yang berumur panjang mengakumulasi tracked entity, meningkatkan penggunaan memori dan memperlambat operasi. ```csharp // Program.cs untuk Blazor Server builder.Services.AddDbContextFactory(options => options.UseNpgsql(builder.Configuration.GetConnectionString("Default"))); ``` Dalam komponen Blazor, inject factory dan buat context berumur pendek: ```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(); } } ``` Panggilan `AsNoTracking()` sangat penting dalam Blazor Server. Tanpa itu, setiap entity yang dimuat tetap berada di change tracker, mengonsumsi memori hingga circuit berakhir. ## Background Service dan Hosted Service Background service yang didaftarkan sebagai singleton tidak dapat meng-inject scoped DbContext secara langsung. Service tersebut outlive scope manapun, menciptakan error ketidakcocokan 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` membuat scope baru untuk setiap iterasi. Scope, dan DbContext di dalamnya, di-dispose di akhir setiap iterasi loop. Alternatifnya, gunakan `IDbContextFactory` untuk kontrol yang lebih granular: ```csharp // OrderProcessingService.cs (versi 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); } } } ``` ## Kesalahan Umum yang Menyebabkan Pelanggaran Thread Safety Beberapa pola secara konsisten menyebabkan masalah thread safety DbContext dalam kode produksi: **Menyimpan DbContext di field static atau singleton**: DbContext tidak boleh outlive scope yang dimaksudkan. Referensi static membuat instance yang sama dapat diakses dari beberapa thread. **Panggilan async fire-and-forget**: Memulai operasi async tanpa meng-await-nya sambil terus menggunakan context di thread saat ini menciptakan akses bersamaan. ```csharp // SALAH: Jangan gunakan public void UpdateAndNotify(int orderId) { var order = _context.Orders.Find(orderId); order.Status = OrderStatus.Shipped; _context.SaveChangesAsync(); // Tidak di-await! _notificationService.SendAsync(order.CustomerId); // Context mungkin masih menyimpan } ``` **Meng-inject DbContext ke service singleton**: Container DI melempar exception dalam development, tetapi beberapa konfigurasi menyembunyikan error ini. **Menggunakan DbContext di beberapa await tanpa memahami alur eksekusi**: Setiap await adalah titik suspensi. Jika kode yang dilanjutkan berjalan di thread berbeda dari yang diharapkan, akses bersamaan dapat terjadi dengan jalur kode lain. ## Panduan Keputusan untuk Konfigurasi Lifetime DbContext Memilih konfigurasi DbContext yang tepat tergantung pada tipe aplikasi dan persyaratan performa: - **Aplikasi web API atau MVC standar**: Gunakan `AddDbContext()` dengan scoped lifetime default. Ini mencakup sebagian besar skenario dengan benar. - **API high-throughput (ribuan request/detik)**: Gunakan `AddDbContextPool()` untuk mengurangi overhead alokasi. - **Aplikasi yang memerlukan query database paralel**: Gunakan `AddDbContextFactory()` atau `AddPooledDbContextFactory()`. - **Aplikasi Blazor Server**: Gunakan `AddDbContextFactory()` dengan context berumur pendek yang dibuat per operasi. - **Background service**: Gunakan `IServiceScopeFactory` atau `IDbContextFactory()` untuk membuat context dalam service. - **Batch processing dengan paralelisme**: Gunakan `AddPooledDbContextFactory()` untuk throughput optimal. Untuk pembahasan lebih mendalam tentang pola async di ASP.NET Core, [modul pemrograman async](/technologies/dotnet/interview-questions/async-aspnet-core) mencakup konsep terkait. [Modul EF Core lanjutan](/technologies/dotnet/interview-questions/ef-core-advanced) memperluas optimasi query dan perilaku change tracking yang dibahas di sini. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/dotnet/dbcontext-lifetime-performance-thread-safety-async