ASP.NET Core'da DbContext Yaşam Süresi: Async İşlemlerde Performans ve Thread Güvenliği

ASP.NET Core'da DbContext yaşam süresi yönetiminde ustalaşın. Scoped vs transient kullanımını, async işlemlerde thread güvenliğini ve DbContext pooling ile performans optimizasyonunu öğrenin.

ASP.NET Core async işlemlerinde DbContext yaşam süresi ve thread güvenliği kalıpları

DbContext yaşam süresi yönetimi, ASP.NET Core uygulamalarında en yaygın hata kaynaklarından birini oluşturmaktadır. Entity Framework Core'un DbContext sınıfı thread güvenli değildir ve async işlemlerde yanlış kullanımı durum bozulmasına, race condition'lara ve tekrarlanması zor istisnalara yol açmaktadır.

DbContext thread güvenli değildir

Tek bir DbContext örneği aynı anda birden fazla thread tarafından kullanılamaz. Eğer bir async metod, aynı bağlamda başka bir işlem başlamadan önce beklenmezse (await), dahili durum bozulur. Bu kural, EF Core 9 dahil tüm EF Core sürümleri için geçerlidir.

Scoped Yaşam Süresinin Web İsteklerinde Neden İşe Yaradığı

ASP.NET Core'un bağımlılık enjeksiyonu, AddDbContext<T>() kullanıldığında DbContext'i varsayılan olarak scoped yaşam süresi ile kaydeder. Her HTTP isteği kendi DbContext örneğini alır ve bu örnek istek tamamlandığında dispose edilir.

Bu yaklaşım iki sorunu çözmektedir: her isteğin izole veritabanı durumuna sahip olmasını sağlar ve bağlam yaşam süresini Unit of Work kalıbıyla uyumlu hale getirir; burada değişiklikler istek işleme sırasında birikir ve sonunda birlikte commit edilir.

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

Scoped yaşam süresi standart istek-yanıt akışları için uygundur. Bir controller action veya Razor Page handler tek bir thread üzerinde çalışır ve tüm async çağrılar düzgün şekilde beklendiği sürece DbContext istek boyunca tutarlı durumda kalır.

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);
    }
}

Temel kısıtlama: her async işlem bir sonraki başlamadan önce tamamlanmalıdır. Yukarıdaki kod doğrudur çünkü SaveChangesAsync metod dönmeden önce beklenmektedir.

Paralel İşlemlerde Thread Güvenliği Sorunu

Geliştiriciler tek bir istek içinde veritabanı işlemlerini paralel hale getirmeye çalıştığında sorunlar ortaya çıkmaktadır. Bu hatalı örneği inceleyelim:

csharp
// HATALI: Kullanmayın
public async Task<IActionResult> GetDashboard()
{
    var ordersTask = _context.Orders.CountAsync();
    var productsTask = _context.Products.CountAsync();
    var customersTask = _context.Customers.CountAsync();

    // Aynı DbContext üzerinde üç sorguyu eşzamanlı çalıştırma
    await Task.WhenAll(ordersTask, productsTask, customersTask);

    return Ok(new { Orders = ordersTask.Result, Products = productsTask.Result, Customers = customersTask.Result });
}

Bu kod, her birini ayrı ayrı beklemeden üç async sorgu başlatmaktadır. Her üç işlem de aynı DbContext örneği üzerinde eşzamanlı çalışarak EF Core'un thread güvenliği gereksinimlerini ihlal etmektedir. Sonuç öngörülemezdir: bazen çalışır, bazen InvalidOperationException fırlatır ve bazen yanlış veri döndürür.

Resmi Microsoft dokümantasyonu async metodların hemen beklenmesi gerektiğini açıkça belirtmektedir. Dahili change tracker, bağlantı durumu ve sorgu derleme önbelleği eşzamanlı erişim için tasarlanmamıştır.

.NET mülakatlarında başarılı olmaya hazır mısın?

İnteraktif simülatörler, flashcards ve teknik testlerle pratik yap.

Paralel Sorgular İçin IDbContextFactory Kullanımı

Paralel veritabanı işlemleri gerçekten gerekli olduğunda, IDbContextFactory<T> çözümü sağlamaktadır. Bu factory, her biri kendi izole durumuna sahip yeni DbContext örnekleri oluşturmaktadır.

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

Factory kaydedildikten sonra, DbContext yerine factory enjekte edilmelidir:

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

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

    public async Task<DashboardStats> GetStatsAsync()
    {
        // Her görev kendi DbContext örneğini alır
        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
        };
    }
}

Her görev kendi bağlamını oluşturur, kullanır ve dispose eder. await using ifadesi, istisna oluşsa bile düzgün temizlemeyi garanti eder.

Yüksek Verimli Uygulamalar İçin DbContext Pooling

Yeni bir DbContext oluşturmak bellek tahsisi, change tracker başlatması ve dahili önbelleklerin kurulumunu içerir. Saniyede binlerce istek işleyen yüksek verimli uygulamalar için bu ek yük ölçülebilir hale gelmektedir.

DbContext pooling, bağlam örneklerini yeniden kullanarak bu sorunu çözmektedir. Havuzlanmış bir bağlam dispose edildiğinde, EF Core durumunu sıfırlar ve garbage collection yerine havuza geri döndürür.

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

Dave Callan'ın benchmark'ları pooling'in mikrobenchmark'larda bağlam oluşturma süresini %90'dan fazla azalttığını göstermektedir. Ancak gerçek dünya etkisi uygulamanın sorgu kalıplarına bağlıdır. Veritabanı sorguları istek işleme süresine hakim oluyorsa, bağlam oluşturma ek yükü kıyasla ihmal edilebilir düzeydedir.

Pooling bir kısıtlama getirmektedir: DbContext durumu havuza dönerken sıfırlanır. DbContext sınıfına eklenen özel alanlar veya özellikler değerlerini kaybedecektir. Change tracker temizlenir, bu nedenle commit edilmemiş değişiklikler kaybolur.

Pooling ile IDbContextFactory'yi Birleştirme

Hem pooling performansına hem de paralel sorgu desteğine ihtiyaç duyan uygulamalar için AddPooledDbContextFactory kullanılmalıdır:

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

Bu kayıt, bağlamların havuzdan geldiği IDbContextFactory<AppDbContext> sağlamaktadır. Her CreateDbContext() çağrısı havuzdan bir örnek alır ve dispose etmek örneği havuza geri döndürür.

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

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

    public async Task ProcessBatchAsync(IEnumerable<OrderUpdate> updates)
    {
        // Havuzlanmış bağlamlarla paralel parçalar halinde işleme
        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'in analizi toplu işleme senaryolarında havuzlanmış factory performansını gösteren ek benchmark'lar sunmaktadır.

Blazor Server: Özel Bir Durum

Blazor Server uygulamaları DbContext yaşam süresi yönetiminde ekstra dikkat gerektirmektedir. Blazor devresi (circuit), net sınırları olan HTTP isteklerinin aksine birden fazla kullanıcı etkileşimi boyunca devam etmektedir.

Varsayılan scoped yaşam süresi sorunlu hale gelmektedir: tek bir DbContext örneği potansiyel olarak saatlerce tüm devre süresi boyunca yaşar. Uzun ömürlü bağlamlar izlenen varlıkları biriktirerek bellek kullanımını artırır ve işlemleri yavaşlatır.

Blazor Server için Program.cscsharp
builder.Services.AddDbContextFactory<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

Blazor bileşenlerinde factory enjekte edilmeli ve kısa ömürlü bağlamlar oluşturulmalıdır:

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() çağrısı Blazor Server'da özellikle önemlidir. Onsuz, yüklenen her varlık devre sona erene kadar change tracker'da kalarak bellek tüketir.

Arka Plan Servisleri ve Hosted Services

Singleton olarak kaydedilen arka plan servisleri scoped DbContext'i doğrudan enjekte edemez. Servis herhangi bir scope'tan daha uzun yaşar ve yaşam süresi uyumsuzluk hataları oluşturur.

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 her iterasyon için yeni bir scope oluşturur. Scope ve içindeki DbContext her döngü iterasyonunun sonunda dispose edilir.

Alternatif olarak, daha ayrıntılı kontrol için IDbContextFactory kullanılabilir:

OrderProcessingService.cs (factory versiyonu)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 Güvenliği İhlallerine Neden Olan Yaygın Hatalar

Üretim kodunda birkaç kalıp sürekli olarak DbContext thread güvenliği sorunlarına neden olmaktadır:

DbContext'i statik alanlarda veya singleton'larda saklama: Bir DbContext asla amaçlanan kapsamından daha uzun yaşamamalıdır. Statik referanslar aynı örneğe birden fazla thread'den erişimi mümkün kılar.

Fire-and-forget async çağrıları: Beklenmeden (await) bir async işlem başlatmak ve aynı anda mevcut thread üzerinde bağlamı kullanmaya devam etmek eşzamanlı erişim oluşturur.

csharp
// HATALI: Kullanmayın
public void UpdateAndNotify(int orderId)
{
    var order = _context.Orders.Find(orderId);
    order.Status = OrderStatus.Shipped;
    _context.SaveChangesAsync(); // Beklenmedi!
    _notificationService.SendAsync(order.CustomerId); // Bağlam hala kayıt yapıyor olabilir
}

DbContext'i singleton servislere enjekte etme: DI container geliştirme ortamında istisna fırlatır, ancak bazı konfigürasyonlar bu hatayı gizler.

Yürütme akışını anlamadan birden fazla await arasında DbContext kullanma: Her await bir askıya alma noktasıdır. Devam eden kod beklenenden farklı bir thread üzerinde çalışırsa, diğer kod yollarıyla eşzamanlı erişim oluşabilir.

Pratik yapmaya başla!

Mülakat simülatörleri ve teknik testlerle bilgini test et.

DbContext Yaşam Süresi Yapılandırması İçin Karar Rehberi

Doğru DbContext yapılandırmasını seçmek uygulama türüne ve performans gereksinimlerine bağlıdır:

  • Standart web API veya MVC uygulaması: Varsayılan scoped yaşam süresi ile AddDbContext<T>() kullanılmalıdır. Bu çoğu senaryoyu doğru şekilde kapsar.
  • Yüksek verimli API (saniyede binlerce istek): Tahsis ek yükünü azaltmak için AddDbContextPool<T>() kullanılmalıdır.
  • Paralel veritabanı sorguları gerektiren uygulama: AddDbContextFactory<T>() veya AddPooledDbContextFactory<T>() kullanılmalıdır.
  • Blazor Server uygulaması: İşlem başına oluşturulan kısa ömürlü bağlamlarla AddDbContextFactory<T>() kullanılmalıdır.
  • Arka plan servisleri: Servis içinde bağlam oluşturmak için IServiceScopeFactory veya IDbContextFactory<T>() kullanılmalıdır.
  • Paralellik ile toplu işleme: Optimal verim için AddPooledDbContextFactory<T>() kullanılmalıdır.

ASP.NET Core'da async kalıplarının daha derin ele alınması için async programlama modülü ilgili kavramları kapsamaktadır. EF Core ileri düzey modülü burada tartışılan sorgu optimizasyonu ve change tracker davranışını genişletmektedir.

Günün meydan okuması

.NET kodundaki hatayı bulabilir misin?

Gerçek bir kod parçası, gizli bir hata, günde bir deneme. Denemek için hesap gerekmez.

Anthony Fillion-Maillet

Yazan:

Anthony Fillion-Maillet

SharpSkill kurucusu

10 yılı aşkın süredir fullstack geliştirici. SharpSkill’i yönetiyor ve burada yayımlanan her şeyden sorumlu.

7 Eylül 2026 tarihinde güncellendi

Etiketler

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

Paylaş

İlgili makaleler