DEV Community

Mustafa  Çam
Mustafa Çam

Posted on

Transient, Scoped, Singleton (Dependency Injection)

C#'ta Dependency Injection (DI), bir sınıfın bağımlılıklarını (yani, dışarıdan alması gereken başka sınıfları veya bileşenleri) dışarıdan sağlayarak yönetilen bir tasarım desenidir. Bu desen, yazılım geliştirme sürecinde bağımlılıkların daha kolay yönetilmesine, test edilebilirliğin artmasına ve sınıfların daha esnek hale gelmesine olanak tanır.

C# ve .NET dünyasında Dependency Injection genellikle Inversion of Control (IoC) Container'lar ile yapılır. ASP.NET Core'da ise DI yerleşik olarak sunulmaktadır ve IoC container'ları ile kolayca entegre edilebilmektedir.

Dependency Injection Türleri

C#'ta DI üç farklı yöntemle yapılabilir:

  1. Constructor Injection (En yaygın yöntem)
  2. Property Injection
  3. Method Injection

1. Constructor Injection

En yaygın kullanılan DI yöntemidir. Sınıfın bağımlılıkları, constructor (yapıcı metot) aracılığıyla sağlanır. Böylece bağımlılıklar sınıfın başlatılması sırasında dışarıdan alınır ve private bir alan olarak saklanır.

Örnek:

Bir IService arayüzüne bağımlılığı olan bir sınıf düşünelim:

public interface IService
{
    void Serve();
}

public class Service : IService
{
    public void Serve()
    {
        Console.WriteLine("Service is called.");
    }
}

public class Client
{
    private readonly IService _service;

    // Dependency Injection via constructor
    public Client(IService service)
    {
        _service = service;
    }

    public void Start()
    {
        Console.WriteLine("Client is starting...");
        _service.Serve();
    }
}
Enter fullscreen mode Exit fullscreen mode

Yukarıdaki örnekte, Client sınıfı bir IService'e bağımlıdır. Bu bağımlılık, constructor ile dışarıdan sağlanmıştır. Service sınıfı ise IService arayüzünü implemente eder.

2. Property Injection

Bağımlılıkların özellikler (properties) aracılığıyla enjekte edilmesidir. Bu yöntem constructor injection kadar yaygın değildir ancak bazı durumlarda kullanışlı olabilir.

Örnek:
public class Client
{
    public IService Service { get; set; }

    public void Start()
    {
        Console.WriteLine("Client is starting...");
        Service?.Serve();
    }
}
Enter fullscreen mode Exit fullscreen mode

Bu örnekte, Client sınıfı IService bağımlılığını bir property olarak alır. Ancak property injection, bağımlılıkların her zaman atanmasını garanti etmediği için constructor injection kadar güvenli değildir.

3. Method Injection

Bağımlılıkların bir metot aracılığıyla enjekte edilmesidir. Bu yöntem daha nadir kullanılır ve bağımlılıkların sadece belirli bir metot içinde gerekli olduğu durumlarda tercih edilir.

Örnek:
public class Client
{
    public void Start(IService service)
    {
        Console.WriteLine("Client is starting...");
        service.Serve();
    }
}
Enter fullscreen mode Exit fullscreen mode

Bu örnekte IService bağımlılığı Start metodu aracılığıyla enjekte edilmiştir.


ASP.NET Core ile Dependency Injection

ASP.NET Core framework'ü yerleşik bir Dependency Injection altyapısı sunar ve DI'yi kullanarak servisler tanımlamak oldukça kolaydır.

1. Servislerin Kayıt Edilmesi

ASP.NET Core'da servisler genellikle Startup.cs veya .NET 6 ile Program.cs dosyasında IoC container'a kaydedilir. Kayıt işlemi şu üç yaşam döngüsünden biriyle yapılır:

  • Transient: Her seferinde yeni bir örnek oluşturulur.
  • Scoped: Her istek (request) için bir örnek oluşturulur ve aynı istek içerisinde paylaşılır.
  • Singleton: Uygulama yaşam döngüsü boyunca tek bir örnek oluşturulur.
Örnek:
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Registering services
        services.AddTransient<IService, Service>();  // Transient lifecycle
        services.AddScoped<IMyScopedService, MyScopedService>();  // Scoped lifecycle
        services.AddSingleton<ISingletonService, SingletonService>();  // Singleton lifecycle
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Dependency Injection ile Kullanımı

Bir sınıfın bağımlılıklarını almak için, o sınıfın constructor'ında bu bağımlılıkları belirtmeniz yeterlidir. ASP.NET Core, ihtiyaç duyulan bağımlılıkları IoC container'dan sağlayacaktır.

Örnek:
public class HomeController : Controller
{
    private readonly IService _service;

    // Dependency Injection via constructor
    public HomeController(IService service)
    {
        _service = service;
    }

    public IActionResult Index()
    {
        _service.Serve();
        return View();
    }
}
Enter fullscreen mode Exit fullscreen mode

Yukarıdaki örnekte, HomeController, IService bağımlılığını constructor aracılığıyla alır ve bu bağımlılık IoC container tarafından otomatik olarak enjekte edilir.

DI ile Lifecycle Yönetimi

Her bir servis kaydı için belirli bir yaşam döngüsü tanımlayabilirsiniz:

  1. Transient: Servis her kullanıldığında yeni bir örnek oluşturulur. Kısa ömürlü servisler için uygundur.
  2. Scoped: Her istek başına bir örnek oluşturulur. Aynı HTTP isteği boyunca aynı servis kullanılır.
  3. Singleton: Uygulama boyunca yalnızca bir kez oluşturulur ve her seferinde aynı örnek kullanılır.
services.AddTransient<ITransientService, TransientService>();
services.AddScoped<IScopedService, ScopedService>();
services.AddSingleton<ISingletonService, SingletonService>();
Enter fullscreen mode Exit fullscreen mode

Özet:

  • Dependency Injection (Bağımlılık Enjeksiyonu) bağımlılıkların dışarıdan sağlandığı bir tasarım desenidir.
  • C# ve ASP.NET Core'da DI en yaygın olarak Constructor Injection yöntemiyle yapılır.
  • ASP.NET Core yerleşik bir DI altyapısı sunar ve servisler Startup.cs dosyasında IServiceCollection aracılığıyla kaydedilir.
  • Bağımlılıkların yaşam döngüleri Transient, Scoped ve Singleton olarak belirlenebilir.

Her ne kadar Singleton belirli durumlarda mantıklı bir tercih gibi görünse de, farklı yaşam döngüleri (Transient, Scoped, Singleton) farklı kullanım senaryoları için tasarlanmıştır. Farklı türlerin var olmasının ana nedeni, her yaşam döngüsünün farklı avantaj ve dezavantajları olmasıdır. İşte bu türlerin neden var olduğunu ve hangi durumlarda kullanıldığını daha iyi anlamanızı sağlayacak bazı açıklamalar:

1. Singleton

  • Açıklama: Uygulama başladığında bir kez oluşturulur ve uygulamanın ömrü boyunca tek bir örnek kullanılır.
  • Ne zaman kullanılır?
    • Stateless (durumsuz) ve paylaşımlı kaynaklar kullanan servislerde mantıklıdır. Örneğin, konfigürasyon ayarları, cache mekanizmaları, loglama servisleri veya veritabanı bağlantı havuzları gibi paylaşılan kaynaklar için ideal olabilir.
  • Avantajlar:
    • Bellek kullanımını azaltır, çünkü her istek için yeni bir nesne oluşturulmaz.
    • Uygulama ömrü boyunca aynı nesne tekrar tekrar kullanılır, bu da performansı artırabilir.
  • Dezavantajlar:
    • Thread-safe (eşzamanlı erişime uygun) olmaları gerekir. Eğer servis bir durum tutuyorsa (stateful) ve çoklu thread'ler tarafından kullanılıyorsa dikkatli olunmalıdır, aksi halde veri tutarsızlıkları yaşanabilir.
    • Paylaşılan veri veya bağımlılıklar arasında senkronizasyon sorunları yaşanabilir.

2. Transient

  • Açıklama: Her istek yapıldığında yeni bir nesne oluşturulur. Servis kısa ömürlüdür ve her kullanımda yeni bir örnek sağlanır.
  • Ne zaman kullanılır?
    • Durum tutan (stateful) ve her kullanımda izole bir nesneye ihtiyaç duyulan işlemlerde tercih edilir. Örneğin, kısa süreli işlemler, kullanıcıya özel verilerle çalışan işlemler, küçük hesaplamalar veya her işlemde temiz bir nesneye ihtiyaç duyulan yerlerde kullanılır.
  • Avantajlar:
    • Her istek için yeni bir nesne oluşturulduğundan, önceki isteklerden kalan verilerle karışıklık yaşanmaz.
    • Nesneler bir duruma (state) sahip oluyorsa, her işlemde temiz bir başlangıç yapılır.
  • Dezavantajlar:
    • Daha fazla bellek ve işlemci gücü tüketir. Her istekte yeni bir nesne oluşturmak, özellikle büyük nesneler için maliyetli olabilir.

3. Scoped

  • Açıklama: Her HTTP isteği (request) başına bir nesne oluşturulur ve bu nesne aynı istek süresince kullanılır. İstek tamamlandığında nesne yok edilir.
  • Ne zaman kullanılır?
    • Kullanıcının veya isteğin belirli bir süre boyunca aynı servis örneğine ihtiyaç duyduğu durumlarda. Özellikle web uygulamaları ve ASP.NET Core projelerinde kullanıcının oturumu süresince bazı işlemleri izole şekilde gerçekleştiren servisler için uygundur. Örneğin, veritabanı işlemleri veya HTTP isteğiyle ilişkili işlemler için idealdir.
  • Avantajlar:
    • Aynı istek boyunca aynı servis kullanılır, böylece veritabanı bağlantıları gibi kaynakların her seferinde tekrar tekrar oluşturulmasının önüne geçilir.
    • Kullanıcının isteğine özel bir servis durumu (state) tutmak mümkündür.
  • Dezavantajlar:
    • Bir HTTP isteği boyunca aynı servis kullanılır, bu da bazı senaryolarda verimsiz olabilir, ancak genellikle web uygulamaları için idealdir.

Neden Hepsi Birlikte Kullanılıyor?

Her bir yaşam döngüsünün farklı bir amacı vardır ve bir uygulamanın farklı ihtiyaçları olduğu için bunların tümü kullanılır:

  1. Singleton: Tek bir örneğin yeterli olduğu, global ve durumsuz (stateless) servislerde.
  2. Transient: Kısa ömürlü ve her işlemde bağımsız bir nesneye ihtiyaç duyulan yerlerde.
  3. Scoped: Özellikle web uygulamalarında, her kullanıcı isteği için bir nesneye ihtiyaç duyulan yerlerde.

Örneğin, bir web uygulamasında:

  • Singleton'ı konfigürasyon ayarları veya loglama gibi global işler için kullanabilirsiniz.
  • Transient'i her kullanıcı isteğinde yeni verilerle çalışan servisler için tercih edebilirsiniz.
  • Scoped'u ise her HTTP isteği boyunca belirli bir veri bağlamında (context) çalışacak servisler için kullanabilirsiniz. Örneğin, her istek için aynı veritabanı bağlantısı kullanılabilir.

Özetle:

Singleton her durumda en mantıklı seçenek değildir, çünkü her kullanım senaryosunda ihtiyaçlar farklı olabilir. Farklı yaşam döngüleri uygulamanın esnekliğini artırır ve kaynak yönetimini daha verimli yapmanıza olanak tanır. Her servis için doğru yaşam döngüsünü seçmek performans, bellek kullanımı ve uygulamanın sağlıklı çalışması açısından kritik öneme sahiptir.

Top comments (0)