.NET'te Dependency Injection: Temel Kavramlar ve Uygulama
# .NET'te Dependency Injection: Temel Kavramlar ve Uygulama
Dependency Injection (DI), ASP.NET Core mimarisinin temel taşıdır. Sonradan eklenen bir özellik değil, framework'ün kendisine -- middleware'den controller'lara, Razor Pages'a kadar -- doğrudan işlenmiş bir tasarım prensibidir. Doğru kullanıldığında modüller arası bağımlılığı azaltır, test edilebilirliği artırır ve kodun evrimini kolaylaştırır. Yanlış kullanıldığında ise yalnızca yük altında ya da production ortamında ortaya çıkan sinsi hatalar üretir.
Büyük .NET projelerinde gördüğüm kadarıyla DI kurulumunun kalitesi, genellikle projenin genel sağlığının güvenilir bir göstergesidir. Lifetime'ları, registration stratejilerini ve composition root kavramını anlayan ekipler, bakımı ve genişletmesi çok daha kolay sistemler üretir.
Service Lifetime'larını Anlamak
ASP.NET Core'un yerleşik container'ı üç farklı yaşam döngüsü sunar. Yanlış seçim, .NET uygulamalarındaki en yaygın hata kaynaklarından biridir.
Transient
Her çözümleme (resolve) işleminde yeni bir instance oluşturulur. Hafif ve durumsuz (stateless) servisler için en güvenli varsayılan seçenektir.
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();Servis paylaşılan bir durum tutmuyorsa ve oluşturma maliyeti düşükse transient tercih edin. Ancak veritabanı bağlantısı açan veya önemli miktarda bellek ayıran servisler için transient israf olabilir.
Scoped
Her istek kapsamında (request scope) tek bir instance oluşturulur. ASP.NET Core'da bu, her HTTP isteği başına bir instance anlamına gelir. Request düzeyinde veriyle çalışan servisler için standart tercihtir.
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IUnitOfWork, EfUnitOfWork>();Entity Framework'ün `DbContext`'i varsayılan olarak scoped kaydedilir ve bunun iyi bir nedeni vardır: tek bir iş birimi (unit of work) boyunca entity değişikliklerini takip eder. İstekler arasında paylaşılması eşzamanlılık sorunlarına ve bayat veriye yol açar.
Singleton
Uygulama ömrü boyunca tek bir instance. İlk çözümlemede oluşturulur ve sonraki her istek için tekrar kullanılır.
builder.Services.AddSingleton<ICacheService, InMemoryCacheService>();
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
ConnectionMultiplexer.Connect(configuration.GetConnectionString(class="code-string">"Redis")!));Singleton'lar thread-safe olmalıdır. Scoped veya transient servislere bağımlı olmamalıdırlar -- aksi takdirde aşağıda açıklanan captive dependency sorunu ortaya çıkar.
Service Kayıt Desenleri
Temel Interface Kaydı
En yaygın ve önerilen desen, interface üzerinden kayıttır. Bu, tüketici ile implementasyon arasında net bir sınır oluşturur.
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<INotificationService, EmailNotificationService>();Çoklu Implementasyon
Aynı interface'in birden fazla implementasyonuna ihtiyaç duyduğunuzda hepsini kaydedip `IEnumerable
builder.Services.AddScoped<INotificationService, EmailNotificationService>();
builder.Services.AddScoped<INotificationService, SmsNotificationService>();
builder.Services.AddScoped<INotificationService, PushNotificationService>();
class=class="code-string">"code-comment">// Tüketici tarafında:
public class OrderProcessor
{
private readonly IEnumerable<INotificationService> _notifiers;
public OrderProcessor(IEnumerable<INotificationService> notifiers)
{
_notifiers = notifiers;
}
public async Task NotifyAll(Order order)
{
foreach (var notifier in _notifiers)
await notifier.SendAsync(order.ToNotification());
}
}Factory Kayıtları
Bazen nesne oluşturma çalışma zamanı kararlarına bağlıdır. Factory delegate'leri bunu temiz bir şekilde çözer:
builder.Services.AddScoped<IPaymentGateway>(sp =>
{
var config = sp.GetRequiredService<IOptions<PaymentOptions>>().Value;
return config.Provider switch
{
class="code-string">"Stripe" => new StripeGateway(config.ApiKey),
class="code-string">"PayPal" => new PayPalGateway(config.ClientId, config.Secret),
_ => throw new InvalidOperationException($class="code-string">"Bilinmeyen ödeme sağlayıcı: {config.Provider}")
};
});Factory kayıtlarını yalnızca nesne oluşturma gerçekten çalışma zamanı dallanmasına ihtiyaç duyduğunda kullanın. Çoğu kayıt için factory yazıyorsanız, bu daha derin bir tasarım sorununa işarettir.
Keyed Services (.NET 8+)
.NET 8, "birden fazla implementasyon" sorununu factory hilelerinden çok daha şık çözen keyed services özelliğini getirdi:
builder.Services.AddKeyedScoped<IStorageService, AzureBlobStorage>(class="code-string">"azure");
builder.Services.AddKeyedScoped<IStorageService, S3Storage>(class="code-string">"aws");
builder.Services.AddKeyedScoped<IStorageService, LocalFileStorage>(class="code-string">"local");
class=class="code-string">"code-comment">// Tüketici tarafında:
public class DocumentService
{
private readonly IStorageService _storage;
public DocumentService([FromKeyedServices(class="code-string">"azure")] IStorageService storage)
{
_storage = storage;
}
}.NET 8 öncesinde bu desen, factory delegate'leri veya wrapper tipleri ile çirkin çözümler gerektiriyordu. Büyük .NET projelerinde gördüğüm kadarıyla keyed services, ekiplerin strategy pattern için yazdığı tekrar eden kodu dramatik biçimde azaltıyor.
Options Pattern
Yapılandırma ağırlıklı servisler için Options pattern, strongly-typed ve doğrulanabilir bir yaklaşım sunar:
class=class="code-string">"code-comment">// Options sınıfını tanımlayın
public class SmtpOptions
{
public const string SectionName = class="code-string">"Smtp";
public string Host { get; set; } = string.Empty;
public int Port { get; set; } = class="code-number">587;
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public bool UseSsl { get; set; } = true;
}
class=class="code-string">"code-comment">// Doğrulama ile kaydedin
builder.Services
.AddOptions<SmtpOptions>()
.BindConfiguration(SmtpOptions.SectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
class=class="code-string">"code-comment">// Serviste enjekte edin
public class SmtpEmailSender : IEmailSender
{
private readonly SmtpOptions _options;
public SmtpEmailSender(IOptions<SmtpOptions> options)
{
_options = options.Value;
}
}`IOptions
Lifetime Tuzakları
Captive Dependency Problemi
Bu, en tehlikeli DI hatasıdır. Captive dependency, daha uzun ömürlü bir servisin daha kısa ömürlü birini yakalaması durumunda ortaya çıkar:
class=class="code-string">"code-comment">// YANLIS: Singleton, scoped bir servisi yakaliyor
builder.Services.AddSingleton<ICacheWarmingService, CacheWarmingService>();
builder.Services.AddScoped<IProductRepository, SqlProductRepository>();
public class CacheWarmingService : ICacheWarmingService
{
private readonly IProductRepository _repo; class=class="code-string">"code-comment">// Bu YAKALANDI
public CacheWarmingService(IProductRepository repo)
{
_repo = repo; class=class="code-string">"code-comment">// Bu scoped instance, singleton kadar yaşayacak
}
}Burada ne oluyor? `SqlProductRepository` tek bir istek boyunca yaşaması için tasarlanmıştı, ama singleton ona sonsuza kadar referans tutuyor. Repository `DbContext` kullanıyorsa, change tracker sınırsızca büyüyecek, bağlantılar açık kalacak ve bellek sızıntıları ile bayat veri göreceksiniz -- genellikle yalnızca production yükü altında fark edilir.
Geliştirme ortamında bunu scope doğrulamasını etkinleştirerek yakalayabilirsiniz:
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = true; class=class="code-string">"code-comment">// Captive dependency'lerde exception fırlatır
options.ValidateOnBuild = true; class=class="code-string">"code-comment">// Tüm kayıtları başlangıçta doğrular
});Geliştirme ortamlarında her ikisini de mutlaka etkinleştirin. Küçük başlangıç maliyeti, önlediği hataların yanında önemsizdir.
Transient Servislerin Dispose Edilmesi
DI container'ı `IDisposable` olan transient servisleri takip eder ve dispose eder. Bu, scope sona erene kadar garbage collect edilmeyecekleri anlamına gelir. Bir scope içinde çok sayıda disposable transient çözümlerseniz bellek birikir:
class=class="code-string">"code-comment">// Dikkatli olun: her çözümlenen instance container tarafından takip edilir
builder.Services.AddTransient<IDbConnection>(sp =>
new SqlConnection(connectionString)); class=class="code-string">"code-comment">// Her biri scope dispose olana kadar tutulurDisposable transient'lar için yaşam döngüsünü kendiniz yönetmeyi veya takip edilmeyen instance döndüren bir factory kullanmayı düşünün.
Service Locator Anti-Pattern
Service locator pattern, iş mantığı içinde `GetService` veya `GetRequiredService` çağrısıyla servis çözümlemek anlamına gelir. Bu bir anti-pattern'dir çünkü bağımlılıkları gizler ve kodu test etmeyi ve anlamayı zorlaştırır.
class=class="code-string">"code-comment">// KOTU: Service locator -- bağımlılıklar gizli
public class OrderProcessor
{
private readonly IServiceProvider _provider;
public OrderProcessor(IServiceProvider provider)
{
_provider = provider;
}
public void Process(Order order)
{
var repo = _provider.GetRequiredService<IOrderRepository>();
var mailer = _provider.GetRequiredService<IEmailSender>();
class=class="code-string">"code-comment">// ...
}
}
class=class="code-string">"code-comment">// IYI: Constructor injection -- bağımlılıklar açık
public class OrderProcessor
{
private readonly IOrderRepository _repo;
private readonly IEmailSender _mailer;
public OrderProcessor(IOrderRepository repo, IEmailSender mailer)
{
_repo = repo;
_mailer = mailer;
}
public void Process(Order order)
{
class=class="code-string">"code-comment">// _repo ve _mailer doğrudan kullanılır
}
}Constructor injection versiyonu sınıfın neye ihtiyaç duyduğunu anında gösterir. Service locator versiyonu bu ihtiyaçları `IServiceProvider` arkasına gizler ve tüm implementasyonu okumadan gerçek bağımlılıkları bilmek imkansız hale gelir.
`IServiceProvider`'ın meşru kullanım alanları vardır: factory kayıtları içinde, middleware'de veya arka plan görevleri için manuel scope oluştururken. Ama domain ve application servislerinde asla görünmemelidir.
Büyük .NET projelerinde gördüğüm kadarıyla service locator kullanımı, bir kez ortaya çıktığında yayılma eğilimindedir. Bir geliştirici kısayol olarak başvurur, diğerleri de onu takip eder. Code review'larda erken yakalamak, ilerideki büyük refactoring'leri önler.
DI ile Test Yazma
Düzgün DI kurulumunun en büyük getirisi test edilebilirliktir. Her bağımlılık constructor üzerinden enjekte edildiğinde, gerçek implementasyonları test double'larıyla değiştirmek son derece kolaydır.
Mock ile Birim Testi
public class OrderServiceTests
{
[Fact]
public async Task PlaceOrder_GecerliUrunlerle_OnayGonderir()
{
class=class="code-string">"code-comment">// Arrange
var mockRepo = new Mock<IOrderRepository>();
mockRepo.Setup(r => r.SaveAsync(It.IsAny<Order>()))
.ReturnsAsync(true);
var mockMailer = new Mock<IEmailSender>();
var service = new OrderService(mockRepo.Object, mockMailer.Object);
class=class="code-string">"code-comment">// Act
await service.PlaceOrderAsync(new Order { Items = testItems });
class=class="code-string">"code-comment">// Assert
mockMailer.Verify(m => m.SendAsync(
It.Is<EmailMessage>(e => e.Subject.Contains(class="code-string">"Onay"))),
Times.Once);
}
}WebApplicationFactory ile Entegrasyon Testi
ASP.NET Core'un `WebApplicationFactory`'si, entegrasyon testlerinde gerçek servisleri test implementasyonlarıyla değiştirmenize olanak tanır:
public class OrdersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public OrdersApiTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
class=class="code-string">"code-comment">// Gerçek veritabanı context'ini kaldırın
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
if (descriptor != null)
services.Remove(descriptor);
class=class="code-string">"code-comment">// Test için in-memory veritabanı ekleyin
services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase(class="code-string">"TestDb"));
class=class="code-string">"code-comment">// E-posta göndericisini sahte implementasyonla değiştirin
services.AddScoped<IEmailSender, FakeEmailSender>();
});
});
}
[Fact]
public async Task PostOrder_CreatedDondurur()
{
var client = _factory.CreateClient();
var response = await client.PostAsJsonAsync(class="code-string">"/api/orders", new
{
Items = new[] { new { ProductId = class="code-number">1, Quantity = class="code-number">2 } }
});
Assert.Equal(HttpStatusCode.Created, response.StatusCode);
}
}Bu yaklaşım, production veritabanınıza dokunmadan ve gerçek e-posta göndermeden tam HTTP pipeline testi sağlar. DI container'ı bu değişiklikleri temiz ve öngörülebilir kılar.
Yaygın DI Hataları
1. Her Şeyi Kaydetmek
Her sınıfın DI'dan geçmesi gerekmez. Basit veri nesneleri, değer tipleri, extension method'lar ve dahili yardımcılar container'da yer almamalıdır. Aşırı kayıt, composition root'u karmaşıklaştırır ve gerçek bağımlılık grafiğini anlamayı zorlaştırır.
2. Constructor'da Aşırı Bağımlılık
Eğer bir constructor dört veya beşten fazla bağımlılık alıyorsa, sınıf muhtemelen Tek Sorumluluk İlkesi'ni ihlal ediyordur. DI bunu görünür kılar -- bu bir özellik, üstesinden gelinecek bir sorun değil:
class=class="code-string">"code-comment">// Bu bir kod kokusu, DI sorunu değil
public class MegaService(
IOrderRepo orders,
IProductRepo products,
IUserRepo users,
IEmailSender email,
ILogger<MegaService> logger,
IPaymentGateway payment,
IInventoryService inventory,
IShippingService shipping) { }Karmaşıklığı gizlemek yerine sınıfı daha küçük, odaklı servislere refactor edin.
3. Container Soyutlamalarını Sızdırmak
Domain veya application katmanınızı `Microsoft.Extensions.DependencyInjection`'a bağlamaktan kaçının. İş mantığınız container'lar hakkında hiçbir şey bilmemelidir:
class=class="code-string">"code-comment">// KOTU: Domain servisi DI container tiplerine bağımlı
using Microsoft.Extensions.DependencyInjection;
public class PricingEngine
{
[FromKeyedServices(class="code-string">"premium")]
public IDiscountStrategy Discount { get; set; } class=class="code-string">"code-comment">// Bunu yapmayın
}DI kablajını composition root'ta tutun -- genellikle `Program.cs` veya `IServiceCollection` üzerindeki extension method'larda.
4. Asenkron Başlatmayı Görmezden Gelmek
Bazı servisler asenkron kurulum gerektirir (veritabanına bağlanma, cache ısıtma). Constructor'da `.Result` veya `.GetAwaiter().GetResult()` ile bloklamayın. Bunun yerine `IHostedService` veya `BackgroundService` kullanın:
public class CacheWarmupService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public CacheWarmupService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var scope = _scopeFactory.CreateScope();
var cache = scope.ServiceProvider.GetRequiredService<ICacheService>();
await cache.WarmAsync(stoppingToken);
}
}`IServiceScopeFactory` kullanarak manuel scope oluşturmaya dikkat edin -- singleton bir background service'ten scoped servislere erişmenin doğru yolu budur.
Composition Root En İyi Uygulamalar
`Program.cs`'inizi, kayıtları extension method'lara gruplandırarak temiz tutun:
class=class="code-string">"code-comment">// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddPersistence(builder.Configuration)
.AddApplicationServices()
.AddInfrastructure(builder.Configuration);
class=class="code-string">"code-comment">// ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddPersistence(
this IServiceCollection services, IConfiguration config)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(config.GetConnectionString(class="code-string">"Default")));
services.AddScoped<IOrderRepository, SqlOrderRepository>();
services.AddScoped<IProductRepository, SqlProductRepository>();
return services;
}
public static IServiceCollection AddApplicationServices(
this IServiceCollection services)
{
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<IPricingEngine, PricingEngine>();
return services;
}
}Bu, uygulama büyüdükçe bile composition root'u okunabilir tutar. Büyük .NET projelerinde gördüğüm kadarıyla bu pattern'i erken benimseyen ekipler, pek çok projenin başına bela olan "500 satırlık Program.cs" sorunundan kurtulur.
Sonuç
İyi DI, sadece container kullanımı değil, açık bağımlılık tasarımıdır. ASP.NET Core'un yerleşik container'ı çoğu uygulama için yeterince güçlüdür ve lifetime'larını, kayıt desenlerini ve kısıtlamalarını anlamak, sizi sinsi ve teşhisi zor hatalardan kurtaracaktır. Bu tasarım, tüm sistem genelinde bakım yapılabilirliği, test edilebilirliği ve değişim güvenliğini doğrudan etkiler.
DI mimarinizi gözden geçirip lifetime risklerini tespit edebilirim -- iletişime geçmekten çekinmeyin.
İlgili Makaleler
ASP.NET Core ile RESTful API Geliştirme
ASP.NET Core ile production-ready REST API geliştirmenin temellerini öğrenin. Controller, routing ve best practice'ler.
.NET'te Clean Architecture: Ölçeklenebilir Proje Yapısı
.NET projelerinde Clean Architecture uygulayın. Katmanlar, bağımlılık yönetimi ve test edilebilir kod için rehber.
.NET Testing: Unit, Integration ve E2E Test Stratejileri
.NET projelerinde test stratejisi oluşturun. xUnit, Moq, integration testing ve test piramidi.
Flutter Projeniz mi Var?
iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.
İletişime Geç