Dependency Injection in ASP.NET

dependency-injection

Yazılım geliştirirken Inversion of Control, IoC Container, Dependency Inversion ve Dependecy Injection kavramlarıyla karşılaşırız. Birbirlerine benzer kavramlar olsalarda ayrımlarının yapılması gerekmektedir. Örnekleriyle tüm terimlere göz atmadan Principle ve Pattern arasındaki farkın anlaşılmasında yarar var.

Design Principle yazılım geliştirirken uygulamaların daha iyi tasarlanması için deklare edilmiş bir takım soyut yönergelerdir. Bu prensipler bir implementasyona tâbi olmaksızın yazılım dillerinden bağımsızdır. SOLID Prensipleri en çok bilinenler arasındadır. Örneğin; Open-Closed Prensibi yazılımların gelişime açık, değişime kapalı olmasını önerir. Bu prensip bir ifadeye dayalıdır, nasıl implemente edileceği bize bağlıdır.

Design Pattern nesneye yönelik yaklaşımda zamanında yaygın şekilde ortaya çıkmış problemlerin nasıl uygulanacağını belirtir. Tasarım desenleri daha önceden test edilmiş ve onaylanmıştır. Prensiplere göre daha spesifiklerdir. Örneğin; uygulama ayağa kaldırıldığında bir sınıfın yalnızca bir örneğiyle çalışılmak isteniyorsa Singleton tasarım deseni uygulanmalıdır.

dependency-injection-asp-net

Yukarıdaki şekilden de görüleceği üzere IoC ve DIP birer prensipken, DI bir tasarım deseni ve IoC Container ise bir framework’tür.

Inversion of Control

IoC, nesneye yönelik dünyada sınıflar arasındaki bağın loosely couple olabilmesi için çeşitli kontrollerin tersine çevrilmesini önerir. Burada kontrol, bir sınıfın sahip olduğu -oluşturulma, çağırılma, bağımlı sınıfların yönetimi ve yok edilmesi gibi- ek sorumlulukları ifade etmektedir. Böylece sınıf ana sorumlulukları harici, yaşam döngüsü gibi görevlerle ilgilenmez.

Günlük hayattan bir örnekle iş yerinize her gün aracınızla gittiğinizi düşünün, bu durumda aracın kontrolü sizde oluyor. IoC prensibi bu senaryoda size iş yerinize taksiyle giderek kontrolü tersine çevirmenizi öneriyor. Böylece taksi şoförü aracı kullanıp araçtan sorumlu olurken siz ana görevinize odaklanabilirsiniz.

tightly-loosely-coupled

IoC, yukarıdaki şekilde anlatıldığı üzere gevşek bağlı kodlar elde etmek için uygulanması gereken ilk adımdır.

Geleneksel üç katmanlı mimariyle ilk örneğe başlayalım. Kullanıcıyla etkileşime geçecek bir UI, iş kurallarını koşturacak bir Business ve veritabanıyla haberleşecek DataAccess katmanlarından oluşan bu projede, biz yalnızca Business ve DataAccess katmanlarına odaklanacağız. İlgili sınıflar aşağıdaki gibi ele alınmıştır.

public class DataAccess
{
    public string GetProductNameById(int id) => "Apple iPhone 14";
}

public class Business
{
    private readonly DataAccess _dataAccess;

    public Business()
    {
        _dataAccess = new DataAccess();
    }

    public string GetProductNameById(int id) => _dataAccess.GetProductNameById(id);
}

Yukarıdaki kod incelendiğinde Business sınıfının bir DataAccess sınıfını örneklediği görülmektedir. Ayrıca Business sınıfı burada örnekleme dışında DataAccess sınıfının yaşam döngüsünü de kontrol etmektedir. DataAccess sınıfında yapılacak bir değişiklik bağlı olduğu tüm sınıfları doğrudan etkileyecektir. Bu sınıflar arasında sıkı (tightly) bir bağ vardır.

Bu sorunları aşmak için Service Locator, Factory, Template Method, Dependency Injection, Abstract Factory ve Strategy tasarım desenleri uygulanabilir. Factory tasarım desenini uygulayarak yapımızı loosely-coupled hale getirelim ve hem maintainable hem de extensible bir yapı elde edelim.

public class DataAccessFactory
{
    public static DataAccess CreateInstance() => new DataAccess();
}

public class Business
{
    private readonly DataAccess _dataAccess;

    public Business()
    {
        _dataAccess = DataAccessFactory.CreateInstance();
    }

    public string GetProductNameById(int id) => _dataAccess.GetProductNameById(id);
}

Artık DataAccess sınıfımızı DataAccessFactory sınıfı üzerinden üretmekteyiz. Business sınıfı bu işlemi DataAccessFactory sınıfına delege ettiğinden kontrol tersine çevrilmiştir. IoC prensibini basit şekilde uyguladık; ancak henüz tamamen gevşek bağlı bir tasarımımız yok.

Dependency Inversion Principle

IoC gibi prensip olan DI prensibi de yine loosely-coupled yapı elde etmeyi amaçlar, birlikte kullanılmaları önerilir. Dependency Inversion prensibi yüksek seviye modüllerin düşük seviye modüllere bağlı olmamasını; bunun yerine her iki modülün de bir abstraction‘a bağlı olmasını önerir.

Yapımızın son şekliyle Business sınıfı hala concrete sınıflarla çalışmaktadır, bu sebeple henüz tam anlamıyla gevşek bir bağa sahip değiliz. Soyutlama işlemi için tahmin edeceğiz üzere interface kullanıyor olacağız. Dependency Inversion prensibini kullanacak arayüzlerimi oluşturalım ve implemente ederek gerekli değişiklikleri yapalım.

public interface IDataAccess
{
    string GetProductNameById(int id);
}

public class DataAccess : IDataAccess
{
    public string GetProductNameById(int id) => "Apple iPhone 14";
}

public class DataAccessFactory
{
    public static IDataAccess GetDataAccessInstance() => new DataAccess();
}

public class Business
{
    private readonly IDataAccess _dataAccess;

    public Business()
    {
        _dataAccess = DataAccessFactory.GetDataAccessInstance();
    }

    public string GetProductNameById(int id) => _dataAccess.GetProductNameById(id);
}

Görüldüğü üzere IDataAccess arayüzü tanımlanmış ve DataAccess sınıfı tarafından implemente edilmiştir. Abstraction işlemi için her iki DataAccessFactory ve Business sınıflarının concrete yapılar yerine abstract varlıklarla konuşması gerektiğinden gerekli tip değişiklikleri de yapılarak prensip uygulanmıştır. Böylece yapımız daha gevşek bir bağa sahip oldu, artık IDataAccess arayüzünü implemente eden farklı bir sınıfla da kolaylıkla çalışabiliriz.

Dependency Injection

Dependency Injection tasarım deseni IoC prensibini implemente ederek bağımlı nesnelerin oluşturulma işlemini tersine çevirir. Bu tasarım deseni bağımlı nesnenin oluşturulma işlemini sınıf dışarısına taşıyarak bağımlı nesnelerin ilgili sınıfa farklı yollardan sunulmasına izin verir.

Bu tasarım deseninde Client, Service ve Injector olmak üzere üç tür sınıf bulunmaktadır.

dependency-injection-class-types

Görüldüğü üzere Injector sınıfı Service sınıfının bir örneğini oluşturarak Client sınıfına inject eder. Böylece Client sınıfının Service sınıfından bir örnek yaratma sorumluluğu ayrılmış olur. Bu inject işlemi üç farklı yaklaşımla yapılmaktadır.

Business sınıfı IDataAccess arayüz örneğini elde etmek için hala DataAccessFactory sınıfına ihtiyaç duymakta ve farklı implementasyonlarla çalışmak sınıfı değişikliğe zorlamaktadır. DI tasarım deseni bağımlı nesneleri Constructor, Property ve Method üzerinden sağlayarak bu sorunu çözmektedir.

Constructor Injection

Injector sınıfı Service sınıfının bir örneğini Client sınıfa constructor üzerinden geçer. Yaygın olarak kullanılan yaklaşımdır. Sınıfımızda gerekli değişiklikleri yapalım.

public class Business
{
    private readonly IDataAccess _dataAccess;

    public Business(IDataAccess dataAccess)
    {
        _dataAccess = dataAccess;
    }

    public string GetProductNameById(int id) => _dataAccess.GetProductNameById(id);
}

public class BusinessService
{
    private readonly Business _business;

    public BusinessService()
    {
        _business = new Business(new DataAccess());    
    }

    public string GetProductNameById(int id) => _business.GetProductNameById(id);
}

Görüldüğü üzere Business sınıfı bağımlı nesneyi constructor üzerinden almaya başlamıştır. Sonrasında BusinessService sınıfı tanımlanmış ve bağlı nesneyi oluşturarak Business sınıfına enjekte etmiştir. Böylece bağlı nesne oluşturma işlemi Business sınıfından kopartılarak yapı daha gevşek bağlı hale getirilmiştir.

Property Injection

Injector sınıfı bağlı nesneyi Client sınıfının public property’si aracılığıyla sağlar.

public class Business
{
    public IDataAccess DataAccess { get; set; }

    public string GetProductNameById(int id) => DataAccess.GetProductNameById(id);
}

public class BusinessService
{
    private readonly Business _business;

    public BusinessService()
    {
        _business = new Business();    
        _business.DataAccess = new DataAccess();
    }

    public string GetProductNameById(int id) => _business.GetProductNameById(id);
}

Görüldüğü üzere Business sınıfı bağımlı nesneyi tanımlanmış property üzerinden almaya başlamıştır. Böylece BusinessService sınıfı bağlı nesneyi ilgili property üzerinden set etmektedir.

Method Injection

Injector sınıfı bağlı nesneyi Client sınıfının public method’u aracılığıyla enjekte eder.

public class Business
{
    private IDataAccess _dataAccess { get; set; }

    public string GetProductNameById(int id) => _dataAccess.GetProductNameById(id);

    public void SetDependency(IDataAccess dataAccess) => _dataAccess = dataAccess;
}

public class BusinessService
{
    private readonly Business _business;

    public BusinessService()
    {
        _business = new Business();
        _business.SetDependency(new DataAccess());        
    }

    public string GetProductNameById(int id) => _business.GetProductNameById(id);
}

Görüldüğü üzere Business sınıfı bünyesinde SetDependency methodunu tanımlamış ve bağımlı nesneyi bu method üzerinden almaya başlamıştır. Böylece BusinessService sınıfı bağlı nesneyi ilgili method aracılığıyla ayarlar.

Bu başlıkta DI ve Strategy tasarım deseni kullanarak loosely-coupled sınıflar elde edildi. Günlük hayatta geliştireceğimiz projeler birçok bağımlı nesneye sahip olacaktır. Her seferinde bu prensip ve tasarım desenlerini uygulamak oldukça efor gerektirecektir.

IoC Container

IoC Container, Dependency Injection tasarım desenini uygulayan framework’lerdir. Bağlı nesnelerin oluşturulması, oluşturulan nesnelerin yaşam döngüleri ve bu nesnelerin bağımlılıklarının yönetilmesinden sorumludur. Böylece bir efor sarfetmek yerine yönetimi container kütüphanelerine devrederek projenin diğer kısımlarına odaklanabiliriz. .NET platformunda Unity, Ninject, StructureMap, Autofac, Castle Windsor gibi çeşitli kütüphaneler bulunmaktadır.

Dependency Injection in ASP.NET

ASP.NET bünyesinde built-in olarak gelen basit bir IoC Container mevcuttur ve IServiceProvider implementasyonuyla varsayılan olarak constructor injection desteklenir.

Container tarafından yönetilen tipler servis olarak adlandırılmaktadır. ASP.NET bünyesinde Framework Services ve Application Services olmak üzere iki tür servis bulunmaktadır. Framework servisleri ASP.NET tarafından kullanımımıza sunulan ILoggerFactory, IHostingEnvironment gibi servislerdir. Application servisleriyse ihtiyaçlarımız doğrultusunda kullanacağımız servislerdir.

Uygulama servislerinin yaşam döngüsü built-in IoC Container tarafından yönetilmektedir. ASP.NET içerisinde Singleton, Transient ve Scoped olmak üzere üç yaşam döngüsü vardır ve belirtilen ömre göre ilgili nesne otomatik olarak dispose edilir.

SingletonUygulama ömrü boyunca tek bir instance oluşturulur ve aynı instance kullanılır.
TransientHer istenildiğinde yeni bir instance oluşturularak kullanılır.
ScopedRequest bazlı instance oluşturulur ve aynı request içerisinde aynı instance kullanılır.

Uygulama içerisinde servislerimizi inject edebilmek için öncelikle IoC Container’a kaydetmemiz gerekmektedir. Aşağıdaki gibi log işlemi gerçekleştirdiğimizi düşünelim.

public interface ILogger
{
    void Log(string log);
}

public class FileLogger : ILogger
{
    public void Log(string log)
    {
        // Logging to the file
    }
}

Uygulama servisleri IServiceCollection tipindeki Services property’si üzerinden eklenmektedir.

builder.Services.Add(new ServiceDescriptor(typeof(ILogger), typeof(FileLogger)));
builder.Services.Add(new ServiceDescriptor(typeof(ILogger), typeof(FileLogger), ServiceLifetime.Scoped));
builder.Services.Add(ServiceDescriptor.Transient(typeof(ILogger), typeof(FileLogger)));

Görüldüğü üzere servis ServiceDescriptor parametresi alan Add methoduyla IoC Container bünyesine kaydedilmiştir. ServiceDescriptor nesnesinde ilk parametre servis tipi ikinci parametre instance olarak tanımlanmaktadır. Buna göre uygulama içerisinde talep edilen servis tipine belirtilen nesne örneği oluşturularak dönülecektir.

builder.Services.AddTransient<ILogger, FileLogger>();
builder.Services.AddScoped<ILogger, FileLogger>();
builder.Services.AddSingleton<ILogger, FileLogger>();

ASP.NET aynı zamanda yaşam döngülerine göre AddSingleton, AddTransient ve AddScoped olmak üzere üç extension method tanımlar. Yukarıda bu methodlar kullanılarak servisler eklenmiştir.

Uygulama servislerimizi kaydettiğimize göre artık bu servisleri nasıl inject edebileceğimize bakalım.

public class HomeController : Controller
{
    private readonly ILogger _logger;

    public HomeController(ILogger logger)
    {
        _logger = logger;
    }

    public IActionResult Index()
    {
        _logger.Log("Injected from [constructor]");

        return View();
    }

    public IActionResult Page([FromServices] ILogger logger)
    {
        logger.Log("Injected from [action method]");

        return View();
    }

    public IActionResult Article()
    {
        var logger = this.HttpContext.RequestServices.GetService<ILogger>();
        logger.Log("Injected [manually]");

        return View();
    }
}

Yukarıdaki kod incelendiğinde ilk olarak Constructor Injection yapıldığı görülmektedir. IoC Container otomatik olarak belirtilen tipte bir instance oluşturacaktır. Bu nesnenin yaşam döngüsü IoC Container tarafından kontrol edilmektedir. İkinci action method içerisinde [FromServices] attribute yardımıyla Method Injection yapıldığı görülmektedir. Üçüncü action method içerisinde talep edilen servise RequestServices property’sinin GetService generic methodu üzerinden manuel olarak erişilmektedir. Built-in container Property Injection özelliğini desteklememektedir.

You may also like...

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir