Mert Özen Her Satırda Daha İleri
Backend .NET Core Web API — #12

Dependency injection derinlemesine: Singleton, Scoped ve Transient lifetime'lar

Mert Özen 8 Ağu 2026 13 dk 61 görüntülenme
Dependency injection derinlemesine: Singleton, Scoped ve Transient lifetime'lar

Dependency injection'ın kalbindeki lifetime kavramını işliyoruz. Singleton, scoped ve transient arasındaki fark, hangisinin ne zaman kullanılacağı ve yanlış seçimin yol açtığı sinsi hatalar.

Bu seride dependency injection'ı hep şöyle bir kullandık: constructor'a bir şey koyduk, .NET onu bize verdi, biz de kullandık. Servisi Program.cs'te kaydettik ama o kayıt satırının detaylarına hiç girmedik. Bugün o perdeyi aralıyoruz. Çünkü servisleri kaydederken verdiğin küçük bir karar, uygulamanın davranışını kökten değiştirebilir ve yanlış seçtiğinde bulması en zor hatalardan birine yol açabilir. Konumuz: lifetime'lar.

Önce Dependency Injection'ı Netleştirelim

Kısa bir hatırlatma. Dependency injection, bir sınıfın ihtiyaç duyduğu şeyleri kendi içinde oluşturmak yerine, dışarıdan hazır olarak almasıdır. UsersController, ihtiyaç duyduğu IUserService'i kendisi new'lemez; onu constructor'ında ister, .NET de gelip eline tutuşturur. Bu "tutuşturma" işini yapan mekanizmaya DI container denir.

Container'ı, servislerin tutulduğu merkezi bir depo gibi düşün. Bir controller "bana IUserService lazım" dediğinde, container depoya bakar ve uygun olanı verir. Peki container bu servisi her istendiğinde yeniden mi üretir, yoksa hep aynısını mı verir? İşte bu sorunun cevabı lifetime'dır.

Lifetime Nedir?

Lifetime, bir servisin ne kadar süre "yaşayacağını", yani container'ın onu ne sıklıkla yeniden oluşturacağını belirler. Bir servisi kaydederken üç seçenekten birini seçersin ve bu seçim, o servisin uygulama boyunca nasıl davranacağını tayin eder. Üç lifetime var: singleton, scoped ve transient. Şimdi her birini tek tek, somut bir benzetmeyle açalım.

Singleton: Tek ve Ortak Örnek

Singleton olarak kaydedilen bir servis, uygulamanın tüm ömrü boyunca yalnızca bir kez oluşturulur. İlk kim isterse container onu üretir, sonra herkese aynı örneği verir. Uygulama kapanana kadar o tek örnek yaşar.

Bunu bir binanın ana giriş kapısı gibi düşün. Binada tek bir ana kapı vardır; içeri giren herkes aynı kapıyı kullanır. Kimseye ayrı bir kapı üretmezsin. Singleton da böyle: herkes aynı örneği paylaşır. Kaydı şöyle yaparsın:

builder.Services.AddSingleton();

Singleton, durum tutmayan (stateless) ve pahalıya mal olan servisler için idealdir; mesela bir yapılandırma okuyucusu ya da bir önbellek. Ama dikkat: paylaşılan tek bir örnek olduğu için, içinde kullanıcıya özel veri tutarsan, o veri yanlışlıkla herkesle paylaşılır. Singleton'ı seçerken "bu servis içinde kişiye özel bir şey saklıyor mu?" diye sormak şart.

Scoped: Her İstek İçin Bir Örnek

Scoped olarak kaydedilen bir servis, her HTTP isteği için bir kez oluşturulur. Aynı istek içinde o servisi kaç kez istersen iste, hep aynı örneği alırsın. Ama yeni bir istek geldiğinde, container yepyeni bir örnek üretir.

Bunu bir restoranda garsonun sana açtığı hesap gibi düşün. Masaya oturduğunda sana bir hesap açılır; o akşam ne sipariş verirsen hepsi aynı hesaba işlenir. Ama yandaki masanın hesabı tamamen ayrıdır ve sen kalkıp gittiğinde hesabın kapanır. Her müşteri (istek) kendi hesabına (örneğine) sahiptir. Kaydı şöyle:

builder.Services.AddScoped();

Scoped, web uygulamalarında en sık kullanılan lifetime'dır ve iyi bir sebebi var. Bir isteğin başından sonuna kadar tutarlı bir durum sağlar; aynı istekteki tüm parçalar aynı örneği paylaşır, farklı istekler ise birbirine karışmaz. EF Core'un DbContext'i tam da bu yüzden scoped kaydedilir: her isteğin kendi DbContext'i olur ve istekler birbirinin verisini bozmaz.

Transient: Her Seferinde Yeni Örnek

Transient olarak kaydedilen bir servis, her istendiğinde yeniden oluşturulur. Aynı istek içinde bile onu iki kez istersen, iki farklı örnek alırsın. En kısa ömürlü lifetime budur.

Bunu kâğıt bardaklar gibi düşün. Su içmek isteyen herkese temiz, yeni bir bardak verirsin; kullanılan bardağı geri almazsın. Her seferinde taze bir tane. Kaydı şöyle:

builder.Services.AddTransient();

Transient, hafif ve durum tutmayan, her çağrıda bağımsız çalışması gereken küçük servisler için uygundur. Ama her seferinde yeni bir örnek üretildiği için, ağır ve pahalı servisleri transient yapmak gereksiz maliyet yaratır. Genelde en az ihtiyaç duyacağın lifetime budur; emin değilsen ilk tercihin transient olmasın.

Yanlış Seçim Neye Yol Açar?

Lifetime'lar teoride masum görünür ama yanlış seçim, bulması en sinir bozucu hatalardan birini doğurur. En klasik tuzak şudur: bir singleton servisin içine, scoped bir servisi enjekte etmek.

Neden sorun? Singleton uygulama boyunca bir kez oluşturulur ve içine aldığı scoped servisi de o an yakalar, sonsuza kadar onu tutar. Oysa scoped servisin (mesela DbContext'in) her istekte yenilenmesi gerekiyordu. Singleton onu dondurduğu için, ilk isteğin DbContext'i uygulama boyunca kullanılmaya devam eder. Bu, veri bozulmalarına ve rastgele görünen hatalara yol açar. Neyse ki .NET bu tehlikeli kombinasyonu genelde başlangıçta yakalayıp seni uyarır, ama mantığını anlamak seni bu tuzaktan baştan korur.

Genel kural şu: bir servis, kendisinden daha kısa ömürlü bir servise doğrudan bağımlı olmamalı. Uzun ömürlü olan (singleton), kısa ömürlü olanı (scoped) içine hapsetmemeli.

Pratik Bir Pusula

Hangi lifetime'ı seçeceğine dair basit bir rehber: durum tutmayan, ağır ve tüm uygulamada paylaşılabilecek servisler için singleton. Bir isteğin ömrü boyunca tutarlı olması gereken, veritabanıyla konuşan servisler için scoped; web API'lerinde bu senin varsayılan tercihin olmalı. Her çağrıda bağımsız ve taze olması gereken hafif servisler için ise transient. Emin olamadığında scoped ile başla; web uygulamalarında en güvenli ve en yaygın seçim odur.

Küçük Bir Deneme

Şunu dene: her istekte üretilip içinde rastgele bir kimlik (mesela bir Guid) tutan basit bir servis yaz. Aynı servisi hem controller'a hem de controller'ın kullandığı başka bir servise enjekte et. Sonra bu servisi sırasıyla singleton, scoped ve transient olarak kaydedip aynı isteğe birkaç kez bakarak kimliğin ne zaman aynı kaldığını, ne zaman değiştiğini gözlemle. Singleton'da hep aynı, scoped'ta istek boyunca aynı ama istekler arası farklı, transient'te ise her çağrıda farklı olduğunu göreceksin. Bu küçük deney, üç lifetime arasındaki farkı herhangi bir tanımdan çok daha net oturtur.

Bir sonraki yazıda global hata yönetimine geçeceğiz: uygulamanın herhangi bir yerinde beklenmedik bir hata oluştuğunda bunu tek bir merkezde nasıl yakalayacağımızı ve kullanıcıya düzgün bir cevap nasıl döneceğimizi konuşacağız. Bugün kavradığın DI mantığı, o merkezi yapıyı kurarken de tam karşımıza çıkacak.