DI Yaşam Döngüleri: Transient, Scoped, Singleton ve Captive Dependency Tuzağı
DI (Dependency Injection) container'ları, nesnelerin ne kadar süre yaşayacağını belirleyen 3 temel lifetime sunar. Ancak bu 3 kavramı ezberlemek yetmez; asıl mesele, hangi servisin hangi servise enjekte edilebileceğini (Lifetime Validasyonu) anlamaktır. Aksi takdirde, uygulamanız sessizce bellek sızdırabilir veya ObjectDisposedException ile çökebilir.
1. Üç Temel Yaşam Döngüsü (Lifetime) Ne İşe Yarar?
-
Transient (Geçici): Her talep edildiğinde (her enjeksiyonda) yeni bir örnek oluşturulur.
-
Kullanım Yeri: Hafif, state'siz (stateless) servisler. Örn: Repository'ler, Helper sınıflar.
-
Uyarı: Çok sık oluşturulurlar, GC'yi yorabilir. Ancak thread-safe olmak zorunda değillerdir çünkü kimse onları paylaşmaz.
-
-
Scoped (Kapsamlı): Bir HTTP isteği (Request) veya bir
IServiceScopeömrü boyunca tek bir örnek oluşturulur. Aynı istek içindeki tüm sınıflar aynı örneği alır.-
Kullanım Yeri: Web API'lerde DbContext (
Entity Framework Core). Bir istek boyunca aynı context kullanılır, böylece aynı istek içindeki değişiklikler (ChangeTracker) birlikte kaydedilir.
-
-
Singleton (Tekil): Uygulama ayağa kalktığında oluşturulur ve uygulama kapanana kadar tek bir örnek olarak kalır.
-
Kullanım Yeri: Cache servisleri, Loglama (ILogger), sabit konfigürasyon (IOptions), statik veri tutucular.
-
Uyarı: MUTLAKA thread-safe olmalıdır. Tüm thread'ler aynı örneğe erişir.
-
2. En Tehlikeli Tuzak: Captive Dependency (Mahkum Bağımlılık)
Bu, DI dünyasının en sinsi sorunudur. Captive Dependency, ömrü daha uzun olan bir servise, ömrü daha kısa olan bir servisin enjekte edilmesi durumudur. Yani "Baba (Singleton), oğlunu (Scoped/Transient) içine hapseder".
-
Örnek (Kötü Kod):
csharp
// Singleton olarak kaydedilmiş bir servis public class CacheService : ICacheService // Singleton { private readonly AppDbContext _dbContext; // Scoped! public CacheService(AppDbContext dbContext) { _dbContext = dbContext; // Captive Dependency! } }
Bu kod neden felakettir?
-
CacheServiceuygulama boyunca (10 dakika, 1 saat) aynı örneği kullanır. -
İçine enjekte edilen
AppDbContextise ilk HTTP isteğinde oluşturulur ve bu Singleton'a yapışır. -
Bu
AppDbContextasla dispose edilmez (çünkü Singleton yaşar) ve tüm uygulama hayatı boyunca aynı connection'ı kullanır. -
İkinci, üçüncü istekler geldiğinde yeni bir
AppDbContextoluşturulmaz; aynı eski, muhtemelen bozulmuş (çünküChangeTrackerşişmiş veya connection closed) context kullanılmaya devam eder. -
Sonuç: Bellek sızıntısı (ChangeTracker büyür), hatalı veri okumaları ve zamanla
Cannot access a disposed objecthataları.
Peki Transient ile Singleton?
Transient de her istekte yenilenir ama Singleton'a enjekte edilirse yine aynı sorun olur. İlk talepte oluşan Transient nesne, Singleton ile birlikte sonsuza kadar yaşar ve asla çöp toplanmaz (Memory Leak).
3. Peki Doğrusu Ne? (Lifetime Kuralları)
DI'nin altın kuralı basittir:
Bir servis, kendisinden daha kısa veya eşit ömre sahip servisleri enjekte edebilir.
| Enjekte Edilen (Dependency) | Singleton | Scoped | Transient |
|---|---|---|---|
| Singleton Host | ✅ Güvenli | ❌ Captive! | ❌ Captive! (Leak) |
| Scoped Host | ✅ Güvenli | ✅ Güvenli | ❌ Dikkat (Scoped ömrü bitince Transient hala referanslanabilir mi?) |
| Transient Host | ✅ Güvenli | ✅ Güvenli | ✅ Güvenli |
Not: Scoped içine Transient enjekte etmek genelde sorun değildir çünkü Transient her çağrıda yenilenir. Ancak Scoped içine Scoped enjekte etmek, aynı istek içinde aynı örneği paylaşmalarını sağlar ki bu genelde istenen davranıştır.
4. Bu Hatayı Derleme Zamanında Nasıl Yakalarım? (Validation)
Microsoft'un DI container'ı (Microsoft.Extensions.DependencyInjection) bu hatayı çalışma zamanında (runtime) fırlatabilir. Ancak bunu uygulama ayağa kalkarken (startup) yakalamak çok daha iyidir.
csharp
// Program.cs veya Startup.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options => ...);
// Captive Dependency kontrolünü AÇ!
builder.Services.AddSingleton<ICacheService, CacheService>(); // Burada hata fırlayacak!
// ValidateScopes: Scope validasyonunu etkinleştir
using var host = builder.Build();
// Bu satır, DI grafiğini tarar ve captive dependency varsa exception fırlatır.
var scopeFactory = host.Services.GetRequiredService<IServiceScopeFactory>();
using (var scope = scopeFactory.CreateScope())
{
// Burada servis çözümlenirken hata patlar!
var service = scope.ServiceProvider.GetRequiredService<ICacheService>();
}
Eğer AddSingleton yerine AddScoped kullanırsanız, bu hata oluşmaz, çünkü Scoped içine Scoped enjekte etmek kurallara uygundur.
5. Peki Gerçekten Singleton İçine Scoped Lazımsa Ne Yapmalı? (IServiceScopeFactory)
Bazen Singleton bir cache servisi, veritabanına yazmak zorundadır. Ama bunu yaparken yeni bir Scoped context oluşturması gerekir. İşte IServiceScopeFactory burada imdadınıza yetişir:
csharp
public class CacheService : ICacheService // Singleton
{
private readonly IServiceScopeFactory _scopeFactory;
public CacheService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task UpdateCacheAsync()
{
// Her çağrıda yeni bir scope oluştur (Yeni DbContext ömrü)
using (var scope = _scopeFactory.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// Artık DbContext her seferinde yenilenir, captive yok!
await dbContext.SaveChangesAsync();
}
}
}
Bu pattern, Singleton içinde Scoped kullanmanın tek doğru ve güvenli yoludur.
6. Gerçek Dünya Performans Notu
-
Transient tahsisi pahalı olabilir. Eğer servisiniz hiçbir state tutmuyorsa, onu Singleton yapmak daha iyidir (thread-safe olduğundan emin olarak).
-
Scoped servisleri
IHostedServiceveyaBackgroundServiceiçinde kullanmayın! Çünkü bu servisler tek bir scope'da çalışır. Eğer arka plan işinde DbContext kullanacaksanız, yineIServiceScopeFactoryile manuel scope oluşturun.
Sonuç:
Projelerinizde AddScoped içine AddSingleton enjekte etmek genelde sorun değildir (Scoped, Singleton'dan daha kısa ömürlüdür, yani kurala uyar). Asıl ölümcül hata, Singleton içine Scoped (veya Transient) enjekte etmektir. Startup'ta ValidateScopes'u aktif edin ve servislerinizi kaydederken şu soruyu sorun: "Bu servis, içine enjekte ettiğim servisten daha mı uzun yaşıyor?" Cevap 'Evet'se, ya lifetime'ları değiştirin ya da IServiceScopeFactory kullanın.