Dependency Injection in ASP.NET
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.
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.
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.
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.
Singleton | Uygulama ömrü boyunca tek bir instance oluşturulur ve aynı instance kullanılır. |
Transient | Her istenildiğinde yeni bir instance oluşturularak kullanılır. |
Scoped | Request 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.