Skip to content

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 IDisposable sorumluluğu ve transient sızıntısı
  • Çoklu kayıt, IEnumerable<T> enjeksiyonu, TryAdd ve 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üresiKaç örnekTipik kullanımDikkat
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)

  1. Kimlik damgası: kurucusunda Guid.NewGuid() üreten ve bunu özellik olarak sunan bir IzServisi yaz. Üç kez kaydet — singleton, scoped, transient — ve bir uç noktada üçünü de ikişer kez enjekte edip Guid'leri yazdır. İki isteği arka arkaya yap: hangi değer değişti, hangisi aynı kaldı?
  2. Esir bağımlılık: singleton bir servise scoped bir servis enjekte et. Development'ta uygulama ne diyor? Hata mesajını oku.
  3. Doğrulamayı zorla: ValidateScopes ve ValidateOnBuild'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?
  4. Atılma: IDisposable uygulayan ve Dispose'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?
  5. Çoklu kayıt: aynı arayüzden üç uygulama kaydet. Önce IArayuz iste (hangisi geldi?), sonra IEnumerable<IArayuz> iste (kaç tane geldi?).
  6. Dekoratör: fabrika kaydıyla bir servisi log dekoratörüyle sar. Sıra önemli mi, hangi sırayla sarıyorsun?
  7. Arka plan: bir BackgroundService yaz ve kurucusunda scoped bir servis istemeyi dene — ne oluyor? Sonra IServiceScopeFactory kalı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."