C# İle Dependency Injection Kullanarak Gevşek Bağlı Mimari Nasıl Yapılır?

C# İle Dependency Injection Kullanarak Gevşek Bağlı Mimari Nasıl Yapılır?
C# İle Dependency Injection Kullanarak Gevşek Bağlı Mimari Nasıl Yapılır?

Gereksinimler ve Ön Hazırlık

Bu rehberdeki örnekleri uygulayabilmek için bilgisayarınızda .NET 8.0 veya daha güncel bir sürümün yüklü olması gerekmektedir. Visual Studio 2022 veya VS Code ile birlikte C# Dev Kit eklentisini kullanmanız önerilir. Uygulamalarımızı bir "Console Application" veya "ASP.NET Core Web API" projesi üzerinde gerçekleştirebiliriz.

  • .NET SDK (Sürüm 8.0 veya üzeri)
  • IDE: Visual Studio 2022 veya VS Code
  • Temel C# arayüz (interface) bilgisi

Sıkı Bağlı Mimari ile Karşılaşma (Neden DI?)

Dependency Injection kullanmadığınızda, bir sınıf ihtiyaç duyduğu nesneyi genellikle new anahtar kelimesiyle kendi içinde üretir. Bu durum, sınıfı o nesneye "sıkı sıkıya" bağlar. Aşağıdaki örnekte, SiparisServisi sınıfı doğrudan Veritabani sınıfına bağımlıdır.

public class Veritabani {
    public void Kaydet(string veri) => Console.WriteLine($"{veri} veritabanına kaydedildi.");
}

public class SiparisServisi {
    private readonly Veritabani _db = new Veritabani(); // Sıkı bağlılık burada oluşuyor
    public void IslemYap(string siparis) => _db.Kaydet(siparis);
}

Bu yapıda Veritabani sınıfını değiştirmek veya test etmek için SiparisServisi sınıfını da değiştirmek zorunda kalırsınız. Bu, ölçeklenebilirliği engelleyen en büyük hatadır.

Adım Adım Gevşek Bağlı Mimariye Geçiş

Gevşek bağlı bir mimari oluşturmak için ilk adım, bağımlılıkları somut sınıflar yerine arayüzler (interface) üzerinden yönetmektir. Bu sayede sınıfınız, verinin nasıl kaydedildiğini bilmez; sadece bir "kaydetme" işlemi olduğunu bilir.

public interface IVeriServisi {
    void Kaydet(string veri);
}

public class SqlVeritabani : IVeriServisi {
    public void Kaydet(string veri) => Console.WriteLine($"SQL: {veri} kaydedildi.");
}

Burada IVeriServisi arayüzünü tanımladık. Artık SiparisServisi sınıfını bu arayüze bağımlı hale getirebiliriz. Bu işleme "Constructor Injection" (Kurucu Metot Enjeksiyonu) denir.

public class SiparisServisi {
    private readonly IVeriServisi _veriServisi;

    public SiparisServisi(IVeriServisi veriServisi) {
        _veriServisi = veriServisi;
    }

    public void IslemYap(string siparis) => _veriServisi.Kaydet(siparis);
}

Dependency Injection Konteynerinin Kurulumu

.NET içerisinde yerleşik olarak gelen Microsoft.Extensions.DependencyInjection kütüphanesi, nesne yaşam döngülerini yönetmenizi sağlar. Uygulamanın başlangıç noktasında (Program.cs) servislerinizi kaydetmelisiniz.

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddTransient();
services.AddTransient();

var serviceProvider = services.BuildServiceProvider();
var siparisServisi = serviceProvider.GetRequiredService();
siparisServisi.IslemYap("Yeni Sipariş");

Bu kod bloğunda AddTransient kullanarak, her çağrıldığında yeni bir nesne üretilmesini sağladık. Bu, DI konteynerinin temel çalışma prensibidir.

Servis Yaşam Döngüleri (Service Lifetimes)

DI kullanırken servislerin ne kadar süre hayatta kalacağını belirlemek performans ve bellek yönetimi için kritiktir. Aşağıdaki tablo, yaşam döngüsü seçeneklerini özetler:

Yaşam Döngüsü Açıklama
Transient Her istendiğinde yeni bir örnek oluşturulur.
Scoped HTTP isteği başına bir örnek oluşturulur.
Singleton Uygulama boyunca tek bir örnek kullanılır.

Güvenlik Uyarısı: Singleton servislerde durum (state) tutarken dikkatli olun. Birden fazla kullanıcı aynı nesneye erişeceği için "thread-safety" (iş parçacığı güvenliği) sorunları yaşanabilir. Veri tabanı bağlantıları gibi kaynaklar için genellikle Scoped tercih edilmelidir.

Yaygın Hatalar ve Debug İpuçları

En sık yapılan hata, bir servisi DI konteynerine kaydetmeyi unutmaktır. Bu durumda çalışma zamanında InvalidOperationException hatası alırsınız. Ayrıca, "Circular Dependency" (Döngüsel Bağımlılık) hatası, A servisi B'ye, B servisi de A'ya ihtiyaç duyduğunda oluşur. Bu, tasarımın hatalı olduğunu gösterir.

// Hatalı Kayıt Örneği: Arayüzü kaydetmeden sınıfı kaydetmek
// services.AddTransient(); // IVeriServisi kayıtlı değilse hata verir!

Debug yaparken IServiceProvider üzerinden nesnelerin doğru çözülüp çözülmediğini kontrol edebilir veya loglama mekanizmalarını kullanarak DI konteynerinin hata mesajlarını inceleyebilirsiniz.

Sıkça Sorulan Sorular

Constructor Injection neden Property Injection'dan daha iyidir?

Constructor Injection, sınıfın ihtiyaç duyduğu bağımlılıkları zorunlu kılar. Sınıf, bağımlılıkları olmadan başlatılamaz; bu da kodun daha güvenli ve öngörülebilir olmasını sağlar.

Singleton kullanmak performansı artırır mı?

Bazı durumlarda evet, ancak her nesneyi Singleton yapmak bellek sızıntılarına ve veri tutarsızlıklarına yol açar. Sadece durum tutmayan veya ağır hesaplama gerektiren servislerde kullanılmalıdır.

DI konteyneri kullanmadan gevşek bağlılık mümkün mü?

Evet, "Service Locator" gibi desenlerle yapılabilir ancak bu desen genellikle "anti-pattern" (kötü uygulama) olarak kabul edilir. DI konteynerleri, modern C# projelerinde standarttır.

Aynı arayüz için birden fazla uygulama (implementation) varsa ne olur?

DI konteyneri son kaydedileni kullanır. Eğer hepsine ihtiyacınız varsa, IEnumerable şeklinde enjeksiyon yaparak tüm uygulamalara erişebilirsiniz.

DI ile birim test nasıl kolaylaşır?

Test yazarken gerçek veritabanı yerine, arayüzü uygulayan bir "Mock" (sahte) nesne enjekte ederek sadece ilgili sınıfın mantığını test edebilirsiniz.

Sorumluluk Reddi: Bu makalede paylaşılan kod örnekleri eğitim amaçlıdır. Üretim ortamında (production) kullanırken, hassas verilerin yönetimi, şifreleme ve güvenlik duvarı gibi katmanları mutlaka gözden geçirin. Kod güvenliği için her zaman en güncel .NET güvenlik yamalarını takip edin.

İleri Seviye DI Teknikleri: Scrutor ile Assembly Taraması

Projeniz büyüdükçe, her yeni servisi Program.cs veya Startup.cs içerisinde tek tek kaydetmek yönetilmesi zor bir hale gelir. Scrutor kütüphanesi, .NET'in yerleşik DI konteynerini genişleterek "Assembly Scanning" (derleme taraması) yapmanıza olanak tanır. Bu sayede, belirli bir isimlendirme kuralına uyan tüm servisleri tek satırda kaydedebilirsiniz.

Öncelikle NuGet üzerinden Scrutor paketini projenize ekleyin. Ardından aşağıdaki yapı ile tüm servislerinizi otomatik olarak kaydedebilirsiniz:

// Program.cs içerisinde
builder.Services.Scan(scan => scan
    .FromAssemblyOf()
    .AddClasses(classes => classes.AssignableTo())
    .AsImplementedInterfaces()
    .WithScopedLifetime());

Bu yaklaşım, yeni bir sınıf eklediğinizde DI konteynerini güncelleme zorunluluğunu ortadan kaldırır. Özellikle büyük ölçekli modüler mimarilerde, "Convention over Configuration" (yapılandırma yerine kural) prensibini uygulamak için idealdir.

DI Konteynerinde Performans Optimizasyonu

Dependency Injection konteynerleri, çalışma zamanında nesne örneklerini oluşturmak için "Reflection" (yansıma) kullanır. Çok yüksek trafikli uygulamalarda, binlerce nesnenin anlık oluşturulması küçük de olsa bir performans maliyeti doğurur. Bu durumu optimize etmek için aşağıdaki stratejileri izleyebilirsiniz:

  • Factory Pattern Kullanımı: Karmaşık nesne oluşturma süreçlerinde Func veya Lazy kullanarak nesnenin sadece ihtiyaç duyulduğunda oluşturulmasını sağlayın.
  • Konteyner Değişimi: .NET'in varsayılan DI konteyneri (Microsoft.Extensions.DependencyInjection) oldukça hızlıdır ancak Autofac veya DryIoc gibi kütüphaneler, daha karmaşık senaryolarda (örneğin; decorator deseni veya daha gelişmiş interception) daha yüksek performans ve esneklik sunabilir.

Aşağıdaki örnek, Lazy kullanarak ağır bir servisin yüklenmesini nasıl erteleyebileceğinizi gösterir:

public class OrderProcessor
{
    private readonly Lazy _reportService;

    public OrderProcessor(Lazy reportService)
    {
        _reportService = reportService;
    }

    public void Process()
    {
        // Servis sadece bu satırda çağrıldığında örneklenecek
        _reportService.Value.GenerateReport();
    }
}

DI ile Decorator Deseni Uygulaması

Gevşek bağlı mimarilerde, mevcut bir servisin davranışını değiştirmeden ona yeni özellikler (loglama, cache, validasyon) eklemek için Decorator deseni kullanılır. .NET DI konteyneri, Scrutor ile birlikte kullanıldığında bu deseni uygulamak oldukça basittir.

// Decorator sınıfı
public class CachedUserService : IUserService
{
    private readonly IUserService _inner;
    public CachedUserService(IUserService inner) => _inner = inner;

    public User GetUser(int id)
    {
        // Cache mantığı buraya gelir
        return _inner.GetUser(id);
    }
}

// Kayıt işlemi
builder.Services.Decorate();

Bu yöntem, "Open/Closed" prensibini destekler; yani IUserService uygulamanızı kod değişikliği yapmadan genişletebilir ve sisteminize yeni yetenekler kazandırabilirsiniz.

Sonuç

C# ile Dependency Injection kullanarak gevşek bağlı bir mimari oluşturmak, projenizin uzun vadeli başarısı için atabileceğiniz en önemli adımdır. Bağımlılıklarınızı arayüzler arkasına gizleyerek kodunuzun test edilebilirliğini artırır ve bakım maliyetlerini düşürürsünüz. Bir sonraki adım olarak, projenizde MediatR kütüphanesini inceleyerek "CQRS" desenini DI ile nasıl birleştirebileceğinizi araştırmanızı öneririm.

Bu yazıya tepkinizi paylaşın:
Mert Özdemir

Teknoloji ve dijital dünyadaki karmaşık süreçleri adım adım rehberlerle sadeleştiriyorum. Kullanıcıların dijital araçlardan en yüksek verimi alması için anlaşılır ve öğretici içerikler yazıyorum.

Yorumlar (0)

Yorum Yaz