Dependency Injection (DI), .NET'in temel taşlarından biri. Ancak lifetime seçimi yanlış yapıldığında veri sızıntısı, thread sorunları ve bellek problemleri kaçınılmaz olur. Bu yazıda DI'ı gerçekten anlamak için gereken her şeyi ele alıyoruz.
Üç Lifetime: Ne Anlama Geliyor?
Singleton
Uygulama boyunca tek bir instance oluşturulur. Tüm request'ler, tüm thread'ler aynı nesneyi paylaşır.
builder.Services.AddSingleton<IConfigService, ConfigService>();
// Singleton için güvenli: thread-safe, readonly state
public class ConfigService : IConfigService
{
private readonly IReadOnlyDictionary<string, string> _settings;
public ConfigService(IConfiguration config)
{
_settings = config.GetSection("App")
.GetChildren()
.ToFrozenDictionary(x => x.Key, x => x.Value ?? "");
}
public string Get(string key) => _settings.GetValueOrDefault(key, "");
}
// Singleton için TEHLİKELİ: mutable shared state
public class BadCounterService
{
private int _count = 0; // Race condition!
public void Increment() => _count++; // Thread-safe değil
// Doğrusu:
private int _safeCount = 0;
public void SafeIncrement() => Interlocked.Increment(ref _safeCount);
}
Scoped
Her HTTP request için bir instance. Request boyunca aynı nesne, request bittikten sonra Dispose edilir.
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddScoped<AppDbContext>(); // EF Core DbContext her zaman Scoped
// Scoped'un tipik kullanımı: birim of work
public class OrderService(AppDbContext db, IPaymentService payment) : IOrderService
{
public async Task<Order> CreateAsync(CreateOrderRequest req)
{
// Aynı request içindeki tüm işlemler aynı db instance'ını paylaşır
var order = new Order(req.CustomerId, req.Items);
db.Orders.Add(order);
await payment.ChargeAsync(order.Total); // Aynı transaction scope'u
await db.SaveChangesAsync();
return order;
}
}
Transient
Her inject edildiğinde yeni bir instance oluşturulur. Durumsuz (stateless), hafif nesneler için uygundur.
builder.Services.AddTransient<ISlugGenerator, SlugGenerator>();
builder.Services.AddTransient<IEmailBuilder, EmailBuilder>();
// Transient uygun: stateless, hafif
public class SlugGenerator : ISlugGenerator
{
public string Generate(string title) =>
title.ToLowerInvariant()
.Replace(" ", "-")
.Replace("ş", "s")
.Replace("ğ", "g");
}
Captive Dependency: En Tehlikeli Anti-Pattern
Uzun ömürlü bir servis, kısa ömürlü bir servise bağımlı olduğunda captive dependency oluşur. Kısa ömürlü servis artık uzun ömürlü kadar yaşar — bu beklenmedik davranışa ve veri sızıntısına yol açar.
// HATA: Singleton, Scoped servise bağımlı
public class ReportService(AppDbContext db) // AppDbContext Scoped!
{
// db, ilk request'te inject edildi ve ömür boyu yaşıyor
// Request 1'in verileri Request 2'ye sızabilir!
}
builder.Services.AddSingleton<ReportService>(); // Hatalı!
// Çözüm 1: ReportService'i de Scoped yap
builder.Services.AddScoped<ReportService>();
// Çözüm 2: IServiceScopeFactory ile scope manuel yönet
public class ReportService(IServiceScopeFactory scopeFactory)
{
public async Task GenerateAsync()
{
await using var scope = scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// db artık bu scope içinde yaşıyor
var data = await db.Orders.ToListAsync();
// scope biter, db dispose edilir
}
}
builder.Services.AddSingleton<ReportService>(); // Artık güvenli
Bu hatayı geliştirme aşamasında yakalamak için:
builder.Services.AddDbContext<AppDbContext>(...);
// Development'ta captive dependency doğrulaması
if (app.Environment.IsDevelopment())
{
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = true; // Captive dependency yakala
options.ValidateOnBuild = true; // Eksik kayıt yakala
});
}
Keyed Services (.NET 8)
Aynı interface'in birden fazla implementasyonu gerektiğinde artık named (keyed) kayıt kullanılabiliyor:
// Farklı cache implementasyonları
builder.Services.AddKeyedSingleton<ICacheService, MemoryCacheService>("memory");
builder.Services.AddKeyedSingleton<ICacheService, RedisCacheService>("redis");
builder.Services.AddKeyedSingleton<ICacheService, NullCacheService>("null");
// Constructor injection ile
public class ProductService(
[FromKeyedServices("memory")] ICacheService localCache,
[FromKeyedServices("redis")] ICacheService distributedCache)
{
public async Task<Product?> GetAsync(int id)
{
// Önce local cache
if (localCache.TryGet<Product>($"product:{id}", out var product))
return product;
// Sonra distributed cache
product = await distributedCache.GetAsync<Product>($"product:{id}");
if (product is not null)
{
localCache.Set($"product:{id}", product, TimeSpan.FromMinutes(1));
return product;
}
return null;
}
}
// Minimal API'de
app.MapGet("/products/{id}", async (
int id,
[FromKeyedServices("redis")] ICacheService cache) =>
{
return await cache.GetAsync<Product>($"p:{id}");
});
Factory Pattern ile Dinamik Kayıt
// Runtime'da karar verilen implementasyon
builder.Services.AddScoped<IStorageService>(sp =>
{
var config = sp.GetRequiredService<IConfiguration>();
var provider = config["Storage:Provider"];
return provider switch
{
"azure" => new AzureBlobStorage(
sp.GetRequiredService<ILogger<AzureBlobStorage>>(),
config["Storage:ConnectionString"]!),
"s3" => new S3Storage(
sp.GetRequiredService<ILogger<S3Storage>>()),
_ => new LocalStorage(
sp.GetRequiredService<ILogger<LocalStorage>>())
};
});
Service Locator Anti-Pattern'ından Kaçının
// Kötü: Service Locator — bağımlılıklar gizli, test edilemez
public class OrderService
{
private readonly IServiceProvider _sp;
public OrderService(IServiceProvider sp) => _sp = sp;
public async Task ProcessAsync(Order order)
{
var repo = _sp.GetRequiredService<IOrderRepository>(); // Gizli bağımlılık!
var email = _sp.GetRequiredService<IEmailService>();
await repo.SaveAsync(order);
await email.SendConfirmationAsync(order);
}
}
// İyi: Constructor injection — bağımlılıklar açık
public class OrderService(IOrderRepository repo, IEmailService email)
{
public async Task ProcessAsync(Order order)
{
await repo.SaveAsync(order);
await email.SendConfirmationAsync(order);
}
}
DI container'ı doğru yapılandırmak, uygulamanın hem güvenilirliği hem test edilebilirliği açısından kritik. Captive dependency kontrolünü development ortamında mutlaka aktif edin; production'a çıkmadan önce bu tür hataları yakalamak çok daha kolay.