Bölüm 4 / 14
Dependency Injection
Container, yaşam süreleri (singleton/scoped/transient).
9 dakikalık okuma
4. hattın DIP dersinde bağımlılıkları elle kurucudan geçirmiş, hepsini kompozisyon kökünde bağlamıştın. ASP.NET Core bu işi yerleşik bir konteynere devreder: sen ne istediğini söylersin, nesne ağacını o kurar. Bu dersin asıl konusu kayıt sözdizimi değil — yaşam süreleri ve onların yol açtığı üretim hatalarıdır. Esir bağımlılık, atılmayan nesneler ve kapsam dışında scoped servis kullanmak; üçü de sözlü sınavın ve gerçek hata kayıtlarının müdavimidir.
01Ne öğreneceksin
- Konteynerin ne yaptığı ve kompozisyon kökünün platformdaki karşılığı
- Üç yaşam süresi: singleton, scoped, transient — hangisi ne zaman
- Kapsam (scope) nedir; web isteğiyle ilişkisi
- Esir bağımlılık ve kapsam doğrulaması
- Konteynerin
IDisposablesorumluluğu ve transient sızıntısı - Çoklu kayıt,
IEnumerable<T>enjeksiyonu,TryAddve keyed servisler - Arka plan servisinde scoped servis kullanmak (
IServiceScopeFactory) - Dekoratör kaydı; servis bulucu anti-deseni; üçüncü parti konteyner ne zaman
02Konteyner ne yapar
DI konteyneri, "şu arayüz istendiğinde şu sınıfı ver" eşlemelerini tutan ve istendiğinde nesne ağacını kuran bir fabrikadır. Elle yazdığın kompozisyon kökü şuydu:
var servis = new SiparisServisi(
new EfSiparisDeposu(new TicaretDbContext(ayar)),
new LoglayanBildirici(new EpostaBildirici(smtp), logger));
Konteynerle aynı şey kayıt + çözümleme olur:
// Program.cs — kompozisyon kökü
builder.Services.AddScoped<ISiparisDeposu, EfSiparisDeposu>();
builder.Services.AddScoped<IBildirici, EpostaBildirici>();
builder.Services.AddScoped<SiparisServisi>();
// Kullanan sınıf — hiçbir şey değişmedi, kurucu enjeksiyonu aynı
public sealed class SiparisServisi(ISiparisDeposu depo, IBildirici bildirici) { /* ... */ }
Kazanç, ağaç büyüdükçe ortaya çıkar: yeni bir bağımlılık eklemek yalnız kurucuya parametre eklemektir, çağıran zincirin hiçbir yeri değişmez. Kavramsal olarak değişen bir şey yoktur — DIP hâlâ senin tasarım kararın, konteyner yalnız bir araç. Konteyner olmadan da DI yapılır; konteyner varken de kötü tasarım yapılabilir.
ASP.NET Core'da barındırıcı yapılandırmayı, loglamayı ve kendi altyapı servislerini
zaten bu konteynere kaydeder; bu yüzden bir denetleyicinin kurucusunda
ILogger<T> ya da IOptions<T> istemen için ek kayıt
gerekmez.
03Üç yaşam süresi
| Yaşam süresi | Kaç örnek | Tipik kullanım | Dikkat |
|---|---|---|---|
| Singleton | Uygulama başına bir | Durumsuz yardımcılar, önbellek, yapılandırma, pahalı istemciler | İş parçacığı güvenli olmalı; paylaşılan durum yarış koşulu üretir |
| Scoped | Kapsam başına bir (web'de: istek başına) | DbContext, depolar, iş birimi, istek bağlamı |
Singleton içine alınamaz; arka planda kapsam elle açılır |
| Transient | Her istendiğinde yeni | Hafif, durumsuz, kısa ömürlü nesneler | IDisposable ise kapsam sonuna kadar birikir |
Kapsam (scope), konteynerin "bu grup nesne birlikte yaşar ve birlikte
atılır" dediği bölgedir. ASP.NET Core her HTTP isteği için bir kapsam açar, isteğin
sonunda kapatır. 12. dersteki Unit of Work'ün çalışma sebebi budur: aynı istekteki tüm
depolar aynı DbContext örneğini paylaşır, çünkü ikisi de aynı kapsamdan
çözümlenir.
Seçim kuralı basittir: durum taşıyorsa ve o durum isteğe aitse scoped; durumsuzsa ve pahalıysa singleton; ikisi de değilse transient. Emin değilsen scoped güvenli varsayılandır — singleton'a terfi ettirmek bir performans kararıdır ve iş parçacığı güvenliğini kanıtlamayı gerektirir.
04Esir bağımlılık ve kapsam doğrulaması
DIP dersinde adı geçen tuzağın tam hâli: uzun ömürlü bir servis, kısa ömürlü bir bağımlılığı içine alırsa o bağımlılık esir düşer ve uzun ömürlüyle birlikte yaşamaya devam eder.
builder.Services.AddSingleton<RaporlamaServisi>(); // uygulama ömrü
builder.Services.AddScoped<ISiparisDeposu, EfSiparisDeposu>(); // istek ömrü
public sealed class RaporlamaServisi(ISiparisDeposu depo); // ← ilk isteğin deposu sonsuza kadar yaşar
Sonuçları: DbContext hiç atılmaz, değişiklik takibi büyüdükçe bellek
şişer, veriler bayatlar ve DbContext iş parçacığı güvenli olmadığı için
eşzamanlı isteklerde "ikinci işlem bu bağlamda başlatıldı" tipi hatalar başlar.
Neyse ki .NET bunu yakalayabilir. Development ortamında kapsam doğrulaması varsayılan olarak açıktır; üretimde de açıkça zorlanabilir:
builder.Host.UseDefaultServiceProvider(o =>
{
o.ValidateScopes = true; // scoped'ı kök kapsamdan çözmeyi hata sayar
o.ValidateOnBuild = true; // eksik/geçersiz kayıtları başlangıçta yakalar
});
ValidateOnBuild, 3. dersteki ValidateOnStart ile aynı
felsefeyi taşır: yapılandırma hatası ilk istekte değil, uygulama ayağa kalkarken
görünsün.
Doğru çözüm, uzun ömürlü servisin kapsamı kendi açmasıdır. Arka plan servislerinde standart kalıp şudur:
public sealed class GunlukRaporServisi(IServiceScopeFactory kapsamFabrikasi) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
using (var kapsam = kapsamFabrikasi.CreateScope()) // her tur kendi kapsamı
{
var depo = kapsam.ServiceProvider.GetRequiredService<ISiparisDeposu>();
await depo.GunlukOzet(ct);
} // kapsam kapanır, DbContext atılır
await Task.Delay(TimeSpan.FromHours(1), ct);
}
}
}
BackgroundService singleton olarak kaydedilir; scoped servisi doğrudan
kurucuya alamaz. Bu kalıbı bilmemek, arka plan işi yazan herkesin er geç düştüğü
hatadır.
05Atılma sorumluluğu ve transient sızıntısı
Kural: nesneyi konteyner ürettiyse, atmaktan da o sorumludur.
IDisposable uygulayan bir servis, ait olduğu kapsam kapanırken atılır:
scoped nesneler istek sonunda, singleton'lar uygulama kapanırken.
Buradaki ince tuzak transient + IDisposable birleşimidir:
transient nesne de bir kapsama bağlıdır ve o kapsam kapanana kadar tutulur. Kök
kapsamdan (yani singleton bir servis içinden) çözümlenen transient disposable'lar
uygulama kapanana kadar birikir — klasik ve teşhisi zor bir bellek sızıntısıdır.
Kaçınma yolu: disposable servisleri transient kaydetmemek, gerçekten gerekliyse kısa
ömürlü kapsam açmak.
// Konteyner atmaz: örneği sen verdiysen ömrü senindir
builder.Services.AddSingleton(new HttpClient()); // önerilmez; 11. derste doğrusu var
// Konteyner atar: tipi kaydettiysen üretim ve atma ona aittir
builder.Services.AddScoped<IDosyaDeposu, DosyaDeposu>(); // DosyaDeposu : IDisposable
İkinci ayrıntı: konteyner, sen hazır örnek verdiğinde (yukarıdaki
AddSingleton(new ...)) o nesneyi atmaz — ömrü sana aittir. Bu ayrım,
"neden Dispose çağrılmıyor" sorusunun en sık cevabıdır.
06Kayıt biçimleri
Temel kayıtların ötesinde bilmen gereken beş biçim:
// 1) Fabrika ile — kurulum mantığı gerektiğinde
builder.Services.AddSingleton<IFiyatServisi>(sp =>
new OnbellekliFiyatServisi(
sp.GetRequiredService<HttpFiyatServisi>(),
sp.GetRequiredService<IMemoryCache>())); // dekoratör kaydı (10. ders)
// 2) Çoklu kayıt — hepsi enjekte edilir (11. dersteki seçici kalıbı)
builder.Services.AddSingleton<IKargoUcreti, StandartKargo>();
builder.Services.AddSingleton<IKargoUcreti, AyniGunKargo>();
public sealed class KargoSecici(IEnumerable<IKargoUcreti> secenekler);
// 3) TryAdd — kütüphane yazarken: kullanıcı kendi kaydını yaptıysa ezme
services.TryAddSingleton<IZamanSaglayici, SistemZamani>();
// 4) Keyed servisler (.NET 8)
builder.Services.AddKeyedSingleton<IKargoUcreti, AyniGunKargo>("ayni-gun");
public sealed class Siparis([FromKeyedServices("ayni-gun")] IKargoUcreti kargo);
// 5) Açık tip generic
builder.Services.AddScoped(typeof(IDepo<>), typeof(EfDepo<>));
Aynı arayüz birden çok kez kaydedilirse son kayıt kazanır (tek örnek
istendiğinde), ama IEnumerable<T> istendiğinde hepsi gelir. Bu
davranış, 11. dersteki strateji seçicisinin çalışma sebebidir.
Bir de yapmaman gereken bir şey var: servis bulucu (service locator). Bir
sınıfa IServiceProvider enjekte edip içeride
GetRequiredService çağırmak, bağımlılıkları kurucudan gizler — sınıfın
neye ihtiyaç duyduğu imzasından okunamaz, testte kurulum zorlaşır ve eksik kayıt
derlemede değil çalışma zamanında patlar. Meşru istisnalar: kompozisyon kökündeki
fabrika ifadeleri ve yukarıdaki IServiceScopeFactory kalıbı.
Üçüncü parti konteyner (Autofac, Lamar) yerleşik konteynerin sunmadığı şeyler için kullanılır: kolay dekoratör kaydı, birleştirme (assembly) taraması, özellik enjeksiyonu, gelişmiş kapsam kuralları. Yerleşik konteyner kasıtlı olarak sadedir ve çoğu uygulama için yeterlidir; üçüncü partiye geçmek somut bir ihtiyaçla gerekçelendirilir.
07Mini alıştırma — Yaşam sürelerini gör
Yaklaşık 20 dakika · dotnet run (web API)
- Kimlik damgası: kurucusunda
Guid.NewGuid()üreten ve bunu özellik olarak sunan birIzServisiyaz. Üç kez kaydet — singleton, scoped, transient — ve bir uç noktada üçünü de ikişer kez enjekte edipGuid'leri yazdır. İki isteği arka arkaya yap: hangi değer değişti, hangisi aynı kaldı? - Esir bağımlılık: singleton bir servise scoped bir servis enjekte et. Development'ta uygulama ne diyor? Hata mesajını oku.
- Doğrulamayı zorla:
ValidateScopesveValidateOnBuild'i açık şekilde ayarla; kayıtlı olmayan bir servisi bir kurucuda iste. Hata ne zaman çıkıyor — başlangıçta mı, ilk istekte mi? - Atılma:
IDisposableuygulayan veDispose'da konsola yazan bir servis yaz. Scoped kaydet, iki istek yap — ne zaman atılıyor? Transient yapıp singleton bir servisin içinden çözümle: atılıyor mu? - Çoklu kayıt: aynı arayüzden üç uygulama kaydet. Önce
IArayuziste (hangisi geldi?), sonraIEnumerable<IArayuz>iste (kaç tane geldi?). - Dekoratör: fabrika kaydıyla bir servisi log dekoratörüyle sar. Sıra önemli mi, hangi sırayla sarıyorsun?
- Arka plan: bir
BackgroundServiceyaz ve kurucusunda scoped bir servis istemeyi dene — ne oluyor? SonraIServiceScopeFactorykalıbıyla düzelt.
08Kendini sına
Üç yaşam süresini ve seçim ölçütünü anlat.
Singleton uygulama başına tek örnektir: durumsuz yardımcılar,
önbellek, yapılandırma ve pahalı istemciler için uygundur, ama iş parçacığı güvenli
olmak zorundadır çünkü paylaşılan durum yarış koşulu üretir. Scoped kapsam başına tek
örnektir; web'de kapsam bir HTTP isteğidir, bu yüzden DbContext, depolar
ve iş birimi scoped kaydedilir — 12. dersteki Unit of Work'ün çalışma sebebi aynı
istekteki tüm depoların aynı bağlamı paylaşmasıdır. Transient her istendiğinde yeni
örnek üretir; hafif ve durumsuz nesneler içindir, ama IDisposable ise
kapsam kapanana kadar birikir. Seçim kuralı: durum taşıyorsa ve o durum isteğe aitse
scoped, durumsuz ve pahalıysa singleton, ikisi de değilse transient. Emin
olunmadığında scoped güvenli varsayılandır; singleton'a terfi bir performans kararıdır
ve iş parçacığı güvenliğini kanıtlamayı gerektirir.
Esir bağımlılık nedir, nasıl yakalanır, nasıl düzeltilir?
Uzun ömürlü bir servisin kısa ömürlü bir bağımlılığı içine
alması ve o bağımlılığın uzun ömürlüyle birlikte yaşamaya devam etmesidir — tipik
örnek, singleton bir servise scoped DbContext/depo enjekte etmektir.
Sonuçları: bağlam hiç atılmaz, değişiklik takibi büyüdükçe bellek şişer, veriler
bayatlar ve DbContext iş parçacığı güvenli olmadığı için eşzamanlı
isteklerde hata alınır. Yakalanması: Development'ta kapsam doğrulaması varsayılan
açıktır; üretimde de ValidateScopes ve ValidateOnBuild
açıkça zorlanır — ikincisi eksik ya da geçersiz kayıtları uygulama ayağa kalkarken
yakalar, yani hata ilk istekte değil dağıtımda görünür. Düzeltme: uzun ömürlü servis
kapsamı kendisi açar. Arka plan servislerinde standart kalıp
IServiceScopeFactory ile her turda CreateScope yapıp scoped
servisi o kapsamdan çözmek ve kapsamı using ile kapatmaktır;
BackgroundService singleton kaydedildiği için scoped servisi doğrudan
kurucuya alamaz.
Konteyner hangi nesneleri atar, hangilerini atmaz?
Kural: nesneyi konteyner ürettiyse atmaktan da o sorumludur.
IDisposable uygulayan servisler ait oldukları kapsam kapanırken atılır —
scoped nesneler istek sonunda, singleton'lar uygulama kapanırken. Sen hazır bir örnek
verdiysen (AddSingleton(new ...)) konteyner onu atmaz; ömrü sana aittir
ve "neden Dispose çağrılmıyor" sorusunun en sık cevabı budur. İnce tuzak
transient + IDisposable birleşimidir: transient nesne de bir kapsama
bağlıdır ve o kapsam kapanana kadar tutulur; kök kapsamdan, yani singleton bir servis
içinden çözümlenen transient disposable'lar uygulama kapanana kadar birikir ve teşhisi
zor bir bellek sızıntısı üretir. Kaçınma: disposable servisleri transient
kaydetmemek, gerekiyorsa kısa ömürlü kapsam açmak.
Aynı arayüzün birden çok uygulaması varken ne yaparsın? Servis bulucu neden anti-desendir?
Aynı arayüz birden çok kez kaydedilirse tek örnek istendiğinde
son kayıt kazanır, IEnumerable<T> istendiğinde hepsi gelir — bu
davranış 11. dersteki strateji seçicisinin temelidir: tüm uygulamalar enjekte edilir
ve kod ya da anahtar üzerinden sözlükle seçilir. .NET 8 ile keyed servisler de vardır:
AddKeyedSingleton ile anahtarlı kayıt, [FromKeyedServices]
ya da GetRequiredKeyedService ile çözme. Kütüphane yazarken
TryAdd* kullanılır, böylece kullanıcının kendi kaydı ezilmez; açık tip
generic kayıt da mümkündür. Servis bulucu — sınıfa IServiceProvider
enjekte edip içeride GetRequiredService çağırmak — anti-desendir çünkü
bağımlılıkları kurucudan gizler: sınıfın neye ihtiyaç duyduğu imzasından okunamaz,
testte kurulum zorlaşır ve eksik kayıt derlemede değil çalışma zamanında patlar. Meşru
istisnalar kompozisyon kökündeki fabrika ifadeleri ve
IServiceScopeFactory kalıbıdır.
09Sınav köşesi
Yazılıda nasıl sorulur
(1) Tanım: DI nedir, IoC nedir, aralarındaki ilişki. (2) En sık çıkan: üç yaşam
süresini yazınız ve birer kullanım örneği veriniz. (3) Eşleştirme:
DbContext hangi yaşam süresiyle kaydedilir ve neden.
(4) Kavram: esir bağımlılık nedir, hangi kombinasyonda oluşur.
(5) Kod okuma: singleton servise scoped bağımlılık enjekte eden kayıt verilip
sorunun bulunması. (6) Sözdizimi: AddSingleton/AddScoped/
AddTransient, fabrika kaydı, TryAdd, keyed servis.
(7) Kavram: servis bulucu neden anti-desendir. (8) Uygulama:
BackgroundService içinde scoped servis nasıl kullanılır
(IServiceScopeFactory). (9) Atılma: konteyner hangi nesneleri
Dispose eder.
Sözlüde nasıl anlatılır — 2 dakikalık taslak
Düzen: (1) Çerçeve — "DI bir tasarım kararıdır, konteyner yalnız araç; DIP'i
uyguladıktan sonra nesne ağacını kurmayı konteynere devrederim, kayıtlar tek yerde,
Program.cs'te durur — kompozisyon kökü budur." (2) Yaşam süreleri —
"singleton uygulama başına, scoped istek başına, transient her istekte yeni.
DbContext scoped'dır, çünkü değişiklik takibi ve işlem sınırı isteğe
aittir." (3) Tuzak — "en sık hata esir bağımlılıktır: singleton servise scoped
bağımlılık verilirse ilk isteğin bağlamı sonsuza kadar yaşar, bellek şişer ve
eşzamanlı erişimde patlar. Bunu ValidateScopes ve
ValidateOnBuild ile başlangıçta yakalarım." (4) Doğru kalıp — "arka
plan servisinde IServiceScopeFactory ile her turda kapsam açar,
using ile kapatırım." (5) Kıdem vuruşu — "çoklu kayıtta
IEnumerable<T> enjeksiyonuyla strateji seçicisi kurarım;
IServiceProvider'ı iş sınıflarına enjekte etmem, o servis bulucudur ve
bağımlılıkları imzadan gizler. Ayrıca disposable servisleri transient kaydetmem —
kök kapsamdan çözülürlerse uygulama kapanana kadar birikirler."
