.NET'te Dependency Injection: Temel Kavramlar ve Uygulama

14 dakika okuma9 Şubat 2026Güncellendi: 9 Mar 2026
Dependency Injection .NETDI .NET Core.NET service lifetimesTransient Scoped Singleton.NET IoC containerConstructor injection C#.NET best practicesLoose coupling .NET

# .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.

csharp
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.

csharp
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.

csharp
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.

csharp
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<INotificationService, EmailNotificationService>();

Çoklu Implementasyon

Aynı interface'in birden fazla implementasyonuna ihtiyaç duyduğunuzda hepsini kaydedip `IEnumerable` olarak enjekte edebilirsiniz:

csharp
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:

csharp
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:

csharp
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:

csharp
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`, `IOptionsSnapshot` ve `IOptionsMonitor` arasındaki farka dikkat edin. İlki, başlangıçta bir kez okuyan singleton'dur. `IOptionsSnapshot` scoped'dur ve her istekte yeniden okur. `IOptionsMonitor` singleton'dur ancak değişiklik bildirimlerini destekler. Yeniden yükleme gereksiniminize uygun olanı seçin.

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:

csharp
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:

csharp
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:

csharp
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 tutulur

Disposable 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.

csharp
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

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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:

csharp
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

Flutter Projeniz mi Var?

iOS, Android ve web için yüksek performanslı Flutter uygulamaları geliştiriyorum.

İletişime Geç