Skip to main content

ASP.NET Core – Dependency Injection

Bu yazıda ASP.NET Core ile bütünleşik olarak gelen Dependency Injection mekanizmasına göz atacağız.

ASP.NET Core ile ilgili daha önceden yayınladığım iki yazı bulunuyor. Eğer ASP.NET Core’a yeni başlangıç yapıyorsanız bu yazıdan önce diğer iki yazıyı da okumanızı tavsiye ederim.

  1. Genel Yapı Hakkında Bazı Notlar: Application Pipeline, Middleware, Services, Host Yapısı
  2. Proje Yapısı, Dosyalar ve Yapılandırmalar

Dependency Injection(DI) nesne-tip bağımlılıklarını en aza indirmek ve gevşek bağlı(loosely coupled) yapılar oluşturmak için sıkça kullandığımız bir yöntemdir. Kullanılan nesnelerin doğrudan bir tipe bağımlı olması yerine, bu tipin kalıtıldığı veya implemente edildiği ana tip(base type) üzerinden bağımlılık oluşturulmasını ve kod içerisinden sadece ana tipin kullanılmasını sağlar.

Not: DI konusunda bilgi sahibiyseniz aşağıda yer alan Bir Örnek Eşliğinde Dependency Injection Nedir? kısmını doğrudan atlayıp daha aşağıda bulunan ASP.NET Core ve Dependency Injection başlığından yazıyı okumaya devam edebilirsiniz.

Bir Örnek ile Dependency Injection Nedir?

Loglama yapmak amacıyla DatabaseLogger adında bir sınıfımız olsun, bütün projede bu sınıfa ait WriteLog vb. metotları kullandığımızı varsayalım. Ancak yarın şartlar değiştiğinde bu sınıf yerine XmlLogger, MessageQueueLogger gibi bir sınıfı kullanmak isteyebiliriz. Bunun için de koddaki tüm DatabaseLogger ifadelerini XmlLogger ile değiştirmemiz gerekir. Yine gerekliyse bu sınıfla ilgili using namespace… gibi tanımlarını da değiştirmek gerekecektir. Yani DatabaseLogger sınıfına doğrudan bağımlı oluyoruz, bu örnek doğru bir çözüm değil tabii ki.

Bunun yerine DatabaseLogger, XmlLogger gibi sınıfları bir ana sınıftan(base class) kalıtırsak veya bir interface’den implemente edersek bu sorunu çözebiliriz. Şöyleki; ILogger adında bir interface’imiz olsun, DatabaseLogger ve XmlLogger sınıfları ILogger interface’ini uygulasın. Kod içerisinde loglama yapacağımız her yerde, uygulama genelinden ulaşabileceğimiz ILogger tipinden oluşan bir nesneyi kullanalım. Bu nesneyi de uygulamanın başlangıcında bir yerlerde

ILogger logger = new DatabaseLogger();

şeklinde oluşturalım. Daha sonradan XmlLogger sınıfını kullanmak istersek yukarıda DatabaseLogger yerine XmlLogger tanımını yapmamız yeterli olacak. Şu an DatabaseLogger sınıfına tek bağımlılığımız bu satır, bağımlılığı daha düşük bir seviyeye indirmiş olduk.

Tabii ki bu yapıyı daha kullanışlı hale getirebilir, sadece bu satıra indirgediğimiz bağımlılığı da ortadan kaldırabiliriz. Örneğin bir config dosyasından hangi loglama tipini kullanacağımızı okuyup, kod içerisinde yapacağımız kontrollerle işlemi dinamik hale getirebiliriz. Eğer “database” ise logger = new DatabaseLogger, eğer “xml” ise logger = new XmlLogger gibi…

Burada bahsettiğimiz en basit haliyle dependency injection’ın uygulanışı. Dependency injection’ı daha sistematik yapmak, nesne-tip bağımlılıklarını kolayca ayarlayabilmek ve nesne referanslarının yaşam sürelerini yönetebilmek gibi işlemler için bazı harici kütüphaneler bulunuyor(IoC Container veya DI Container dediğimiz şey). .NET dünyasında Unity, Castle Windsor, Ninject, Autofac, StructureMap gibi DI container’ların daha yaygın şekilde kullanıldığını söyleyebiliriz.

DI konusuna burada bir nokta koyalım ve asıl konumuza geçelim.

ASP.NET Core ve Dependency Injection

ASP.NET Core’da bütünleşik(build-in) bir dependency injection mekanizması bulunmaktadır. Bu bütünleşik DI yapısı basit ve minimum isterleri sağlamak amacıyla inşa edilmiştir, Bu mekanizma ile uygulama başlangıcında belirli nesneleri Service yapısına kaydederek, bu nesnelere uygulama genelinden kolayca erişebiliriz. Bu yöntem nesne bağımlılıklarını ortadan kaldırmamızın yanında, tekrar kullanılabilir(reusable) nesneler oluşturmamıza, nesneleri merkezi bir yerden yönetmemize ve nesnelere uygulama genelinden aynı şekilde erişmemize olanak sağlar. Harici bir DI container’ı ile uğraşmamak, az kod, az efor, temiz kod, stabil nesneler, sade ve anlaşılır bir yaklaşımla bağımlılıkları yöntebilmek…

Service yapısına daha önceki bir yazımda değinmiştim. .NET Framework ile geliştirdiğimiz uygulamalarda bütünleşik bir DI yapısı bulunmuyordu, ihtiyaç duymamız halinde yukarıda saydığımız DI container kütüphanelerinden birini kullanarak nesne bağımlılıklarını yönetebiliyorduk. .NET Core’da ise bu DI yapısı bütünleşik olarak framework içerisinde bulunuyor, hatta ASP.NET Core’da kendi nesnelerimiz için olmasa da .NET Core runtime’ında bulunan bazı yapılar için bu mekanizmayı kullanmamız zorunlu hale geliyor.

DI ile yönettiğimiz Service mekanizmasına dahil edilen yapıları ikiye ayırabiliriz:

Framework Servisleri: .NET Core içerisinde yer alan ve uygulama genelinde kullanılacak bileşenlerdir. Örnek olarak Mvc, Entity Framework, Identity servislerini verebiliriz. Bu servisler genellikle Add… ile başlayan metotlar aracılığıyla Service yapısına dahil edilir.

Kendi Tiplerimiz: Kendi yazdığımız veya harici bir kütüphaneden kullandığımız tiplerin bağımlılık yapılandırmaları için yine Service yapısını kullanabiliriz. Örneğin ILogger adından bir interface tipimiz varsa bu tipi ve tipe ait bir nesne referansını(ör: DatabaseLogger) bu yapıya dahil edebiliriz.

Startup sınıfındaki ConfigurationServices metodunda servis bileşenleri ve nesne bağımlılıkları bu yapı içerisine kaydediliyoruz. Bu işlemler ConfigurationServices metodunun parametresi olan IServiceCollection nesnesi ile yapılıyor. Aşağıdaki kod örneğinde Service yapısına Mvc ve Entity Framework bileşenleri ile kendi oluşturduğumuz bir tip bağımlılığının nasıl eklendiğini görebilirsiniz.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
    services.AddDbContext<NorthwindContext>();
 
    services.AddSingleton<ILoggerDatabaseLogger>();
}

Mvc servislerini AddMvc, DbContext olarak NorthwindContext tipinini de AddDbContext metodu ile Service yapısına eklemişiz. Son adımda AddSingleton metodu ile kendi oluşturduğumuz DatabaseLogger sınıfına ait bir nesneyi singleton olarak ILogger tipine bağlamışım, yani uygulamada DI yapısı üzerinden erişilen tüm ILogger nesne referansları aslında bir DatabaseLogger nesnesi olarak gelecek.

Buradaki DI register metotlarına ek olarak bir de Configure metodu bulunuyor. appsettings.json dosyasındaki bileşenlere erişmek için yazılan class’larımızı bu metot üzerinden DI yapısına enjekte edebiliyoruz. Configure metodunu ve bu konunun detayını -fırsat bulup yazabilirsem- bir sonraki yazımda detaylıca anlatacağım.

DI yapısına enjekte ettiğimiz nesnelere uygulamanın Controller sınıflardaki constructor metotlarından erişebiliyoruz. Bunun için Controller sınıfının ctor metoduna DI’a enjekte edilen ana tipi bir parametre olarak geçirmemiz gerekiyor. Bu yönteme DI kültüründe constructor injection adı veriliyor. ctor metoduna istediğimiz sayıda parametre geçebiliriz.

Runtime’da .NET Core, DI yapısına bağlanmış nesneleri bu metoda parametre olarak geçirecektir. Tabii ki parametreden gelen nesneyi class içerisinde kullanabilmek için yine aynı tipten bir field tanımlamak gerekiyor. Aşağıda ProductController isimli sınıfta yapılan düzenlemeleri görebilirsiniz.

public class ProductController : Controller
{
    NorthwindContext _context;
    ILogger _logger;
 
    public ProductController(NorthwindContext context, ILogger logger)
    {
        _context = context;
        _logger = logger;
    }
 
    public IActionResult List()
    {
        var model = _context.Products.ToList();
        return View(model);
    }
}

Yukarıda ctor metoduna DI yapısına enjekte ettiğimiz NorthwindContext ve ILogger nesnelerini Controller sınıfına constructor injection yöntemiyle taşıyoruz. Sınıf içinde tanımladığımız aynı tipteki iki field’a bu değerleri atayıp artık action metotlarında kullanabiliriz.

Controller sınıflarında ctor’a parametre aktarmadan sadece belirli bir action metodunda DI yapısındaki bir nesneyi kullanmak istersek, metoda geçirilen parametrenin başına FromServices attribute’unu eklememiz gerekiyor. Aşağıda bu attribute’un kullanımına dair bir örnek bulunmaktadır.

public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
 
    public IActionResult Error([FromServices]ILogger logger)
    {
        logger.WriteLog("Bilinmeyen bir hata alındı");
        return View();
    }
}

Error metoduna ILogger interface’ine bağlı nesneyi aktarmak için logger parametresinin başına [FromServices] şeklinde attribute’u ekliyoruz. Artık bu metot içerisinde Service’den gelen nesneyi kullanabiliriz. Constructor’a parametre geçirilmek istenmeyen durumlarda veya Controller’daki sadece bir metotta Service’deki bir nesneyi kullanılmak istediğimiz durumlarda bu yöntemi kullanabiliriz.

View’lardan Service Nesnelerine Erişim

Controller sınıflarından Service nesnelerine eriştiğimiz gibi view dosyalarından da bu nesnelere erişmemiz gerekebilir. ASP.NET Core’da view’larda kullanabileceğimiz @inject directive’i ile DI yapısına enjekte edilmiş nesnelere ulaşabiliyoruz. Aşağıda bu kullanım için bir örnek paylaşıyorum.

@model List<AspNetCoreSamples.DI.Model.Products>
@inject ILogger logger
 
@{ 
    logger.WriteLog("Product/List sayfası çalıştı");
}

 

DI Yapısına Enjekte Edilen Tipler için Nesne Yaşam Süreleri

Önceki kısımlarda bahsettiğim gibi kendimize ait tipleri veya harici kütüphanelerden gelen tipleri de ASP.NET Core’un DI yapısına enjekte edebiliyoruz. Bu şekilde tip bağımlılıklarını projenin giriş noktası olan ConfigureServices metodundan yönetmemiz mümkün oluyor.

Yukarıdaki örnekte ILogger tipi için singleton bir nesne oluşturmuştuk. Singleton ile birlikte Transient ve Scoped olmak üzere nesne yaşam süresine karar verebileceğimiz 3 farklı yöntem bulunuyor. Aşağıda bu yöntemleri kısaca açıklamaları bulunuyor:

Transient: Nesneye yapılan her çağrıda yeni bir nesne oluşturulur. Stateless nesneye ihtiyaç duyulan durumlarda kullanılır. AddTransient() metodu aracılığıyla Transient tipinde bağımlılıklar oluşturabiliriz.

Scoped: Yapılan her request’te nesne tekrar oluşur ve bir request içerisinde sadece bir tane nesne kullanılır. Bu yöntem için de AddScoped() metodu kullanılıyor. Transient ve Scoped kullanım şekilleri nesne oluşturma zamanları açısından biraz karıştırılabilir. Transient’da her nesne çağrımında yeni bir instance oluşturulurken, Scoped’da ise request esnasında yeni bir instance oluşur ve o request sonlanana kadar aynı nesne kullanılır. Request bazında stateless nesne kullanılması istenen durumlarda Scoped bağımlılıkları oluşturabiliriz.

Singleton: ASP.NET Core uygulaması başlatıldığında register edilen nesneye ait sadece bir tane instance oluşur ve uygulamadaki her yerden bu referans çağrılır. Uygulama yeniden başlatılana kadar bu nesne referansı kullanılır, farklı bir nesne referansı ikinci kez oluşturulmaz. Bu yöntem için de AddSingleton() metodunu kullanıyoruz.

ASP.NET Core ile bütünleşik olarak gelen dependency injection ile ilgili genel konuları ele aldık. İlerleyen zamanlarda fırsat bulabilirsem yine bu tarz konularla ilgili birkaç yazı daha yazmaya çalışacağım. Takipte kalalım.

http://umutluoglu.com/feed/
https://twitter.com/umutluoglu

aspnet-core-logo

ASP.NET Core – Dependency Injection” hakkında 3 yorum

  1. On numara makale. Teşekkürler Uğur bey.
    Ben yeni bir projeye başlayacağım. MVC 5 ile mi baslasam(3-4 yıldır mvcdeyim) yoksa core a mi geçsem diye düşünüyorum. 5 deki herşeyi yapabilir miyim? Sizce geçersem ne gibi sorunlar yasarim?

  2. .NET Core henüz olgunlaşma sürecini tamamlamadı. Linux üzerinde uygulama çalıştırma gereksinimin veya saniyede 30-40 bin tane request’i işleyebileyim.. gibi bir ihtiyacın yoksa ben ASP.NET MVC kullanmanı tavsiye ederim.

    ASP.NET Core temel olarak çoğu ihtiyacını karşılar, ancak henüz bazı kütüphaneler için desteği yok veya kısıtlı desteği var. Hobi amaçlı bir proje yapacaksan veya müşteri karşısına çıkacak küçük çaplı bir proje yapacaksan ASP.NET Core kullanabilirsin. Diğer durumlarda ASP.NET MVC(.NET Framework) kullanmaya devam edebilirsin bir süre daha.

    Not: Yorumu ilk yazdığımda ASP.NET 5 gibi okuyup ona göre yorum yazmışım, öyle değilmiş. İlk cümlemi yorumdan çıkardım :)

Bir Cevap Yazın

E-posta hesabınız yayımlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir