Asp.NET Core – Constructor Injection Hell Durumuna Karşılık Alternatif Çözümler
Merhaba,
Asp.NET Core uygulamalarında, sınıfların instance’larını Inversion of Control prensibi gereği yönetebilmek için IoC Container kullanılmaktadır. Bu container sayesinde sınıf nesnelerinin oluşturulması, silinmesi ve kullanım ömrü gibi yapılandırmalar sağlanarak bu sınıflara olan bağımlılık büyük ölçüde azaltılmakta, yeniden kullanım ve test edilebilirlik basitleştirilmektedir. IoC container’ın bizlere sağladığı bu avantajların yanında, kimi controller’lar da Constructor Injection Hell adı verilen haddinden fazla instance talepleri söz konusu olabilmektedir. Bu içeriğimizde, bu durumlara istanaden nasıl bir yaklaşım sergilenmesi gerektiğini, biryandan durumları simüle ederek farklı kütüphaneler eşliğinde izah etmeye çalışıyor olacağız. Buyrun başlayalım.
Constructor Injection Hell Nedir?
Klasik Asp.NET Core uygulamalarında IoC Container’dan bir nesneyi talep edebilmek için Constructor Injection dediğimiz aşağıdaki davranışı sergilemekteyiz.
[ApiController] [Route("api/[controller]")] public class FooController : ControllerBase { private readonly ILogger<FooController> _logger; private readonly IOrderRepository _orderRepository; private readonly IProductRepository _productRepository; private readonly IRoleManager _roleManager; private readonly IUserManager _userManager; private readonly IUserRepository _userRepository; . . . . public FooController( ILogger<FooController> logger, IOrderRepository orderRepository, IProductRepository productRepository, IRoleManager roleManager, IUserManager userManager, IUserRepository userRepository, . . . .) { _logger = logger; _orderRepository = orderRepository; _productRepository = productRepository; _roleManager = roleManager; _userManager = userManager; _userRepository = userRepository; . . . . } }
Normal şartlarda 3 – 5 kadar adil sayıdaki servisin constructor injection ile talep edilmesi gayet normal ve kabul edilebilir bir yaklaşımdır. Lakin ilgili yazılımın geliştirme süreci uzadıkça ve kapsamı genişledikçe inject edilen bu servisler yukarıdaki satırlarda da bahsedildiği gibi haddinden fazla sayıda olabilirler. Hal böyle olunca bir controller’ın bu derece injecte boğulması Constructor Injection Hell yani Constructor Injection Cehennemi olarak nitelendirilmektedir. Bir controller, içerisindeki action’lara odaklı bir şekilde geliştirilmekten ziyade hacminin büyük kısmını inject operasyonlarına ayırıyorsa bu durum geliştirme sürecini olabildiğince olumsuz etkileyebilmektedir. Misal olarak, onlarca inject arasından aradığınız servisin olup olmadığını görebilmek yahut var olanı kaldırabilmek için gözünüzle ciddi bir efor sarfetmeniz gerekebilmektedir.
Ayrıca constructor injection hell; yazılan kodun, kötü kodlara(bad code) karşılık temiz tutulması için uyulması gereken 10/100 kuralına aykırı bir durum ortaya çıkarmaktadır. Neydi bu 10/100 kuralı? diye sorarsanız eğer;
- 1- Bir metot 10 satıra ulaşırsa eğer durmanız ve metodu tekrar değerlendirip refactoring yapmanız gerekir.
- 2- Bir sınıf 100 satıra ulaşırsa(yorumlar ve parantezler dahil) eğer durmanız ve sınıfı tekrar değerlendirip refactoring yapmanız gerekir.
Haliyle constructor injection hell durumunda her ne kadar özelde olsa özünde yine bir metot olan constructor 10/100 kuralını refactoring yapılamayacak şekilde çiğnemektedir.
Hele hele bu tarz constructor’ı şişirilmiş yapılanmalarla test süreçlerine girdiğinizi düşürseniz her bir parametreye karşılık vereceğiniz değerleri yönetebilmenin ayrı bir zahmet olacağı aşikar olsa gerek.
Çözüm Olarak Önerilen Nedir?
Constructor Injection Hell’e karşı çözüm olarak en etkili yöntem instance yönetiminin bir servis tarafından sağlanmasıdır. Bunu sağlayan servisin yaptığı işleme bizler Automatic Factory diyeceğiz. Bunun için olgun bir IoC Container olan Castle Windsor‘dan istifade ederek çözüm getirebiliriz. Yahut Asp.NET Core’da gelen default/build-in IoC container için Castle Windsor’a benzer automatic factory sorumluluğunu üstlenmesini sağlayan AspNetCoreInjection.TypedFactories kütüphanesini de kullanabiliriz.
Şimdi gelin, her iki kütüphane ile automatic factory operasyonunu gerçekleştirelim. Bunun için her iki yöntemde de bir önceki paragrafta bahsedilen instance yönetimini sağlayacak olan servisin arayüzünü tanımlamamız gerekmektedir. Bu interface, IoC container’da istenilen instance’ı elde edebilmemiz için resolve ederek verebilecek kabiliyette bir arayüz olacaktır. Bu arayüzlere factory interface diyeceğiz.
public interface IRepositoryFactory { IOrderRepository CreateOrderRepository(); IProductRepository CreateProductRepository(); IUserRepository CreateUserRepository(); }
Tabi bizler IoC container’da sadece repository servislerini değil, diğer servislerimizin instance’larını da barındırıyor olacağımızdan dolayı bunlarıda farklı bir interface ile temsil etmek isteyebiliriz. Böyle bir durumda istenildiği kadar factory interface oluşturulabilir.
public interface IServiceFactory { IRoleManager CreateRoleManager(); IUserManager CreateUserManager(); }
- AspNetCoreInjection.TypedFactories Kütüphanesi İle Automatic Factory
İlk olarak projeye AspNetCoreInjection.TypedFactories kütüphanesini yükleyiniz.Ardından ‘Program.cs’ dosyasına gelerek aşağıdaki register işlemlerini gerçekleştiriniz.
. . . builder.Services.RegisterTypedFactory<IRepositoryFactory>() .Flavor<IOrderRepository, OrderRepository>() .Flavor<IProductRepository, ProductRepository>() .Flavor<IUserRepository, UserRepository>() .Register(); builder.Services.RegisterTypedFactory<IServiceFactory>() .Flavor<IRoleManager, RoleManager>() .Flavor<IUserManager, UserManager>() .Register(); . . .
Bu işlemlerden sonra tek yapılması gereken ilgili instance’ların yönetimini üstlenen arayüzlerin dependency injection ile talep edilmesi ve bu arayüzler eşliğinde hedef instance’ların üretilmesi için ilgili metotların çağrılmasıdır.
public class FooController : ControllerBase { private readonly IRepositoryFactory _repositoryFactory; private readonly IServiceFactory _serviceFactory; private readonly IProductRepository _productRepository; private readonly IOrderRepository _orderRepository; private readonly IUserRepository _userRepository; private readonly IRoleManager _roleManager; private readonly IUserManager _userManager; public FooController(IRepositoryFactory repositoryFactory, IServiceFactory serviceFactory) { _repositoryFactory = repositoryFactory; _serviceFactory = serviceFactory; _productRepository = _repositoryFactory.CreateProductRepository(); _orderRepository = _repositoryFactory.CreateOrderRepository(); _userRepository = _repositoryFactory.CreateUserRepository(); _roleManager = _serviceFactory.CreateRoleManager(); _userManager = _serviceFactory.CreateUserManager(); } . . . }
İşte bu kadar 🙂
- Castle Windsor IoC Container İle Automatic Factory
Castle Windsor ile automatic factory’i gerçekleştirebilmek için öncelikle aşağıdaki kütüphanelerin projeye yüklenmesi gerekmektedir.Ardından ‘Program.cs’ dosyasında aşağıdaki konfigürasyon gerçekleştirilmelidir.
. . . builder.Host .UseServiceProviderFactory(new WindsorServiceProviderFactory()) .ConfigureContainer<WindsorContainer>((context, container) => { container.Kernel.AddFacility<TypedFactoryFacility>(); container.Kernel.Register( Component.For<IServiceFactory>().AsFactory(), Component.For<IRepositoryFactory>().AsFactory() ); }); . . .
Bu konfigürasyondan hasbel kader bahsetmemiz gerekirse eğer; 5. satırda
UseServiceProviderFactory
metodu aracılığıyla ‘WindsorServiceProviderFactory’nin kullanılacağı bildirilmekte ve 6. satırda ise container olarak ‘WindsorContainer’ eklenerek konfigürasyonları gerçekleştirilmektedir. Bu konfigürasyonlar, 10 ve 11. satırlardaFor
fonksiyonları ile eklenen instance yönetiminden sorumlu arayüzlerin eklenmesinden ibarettir.Tabi Castle Windsor için yapılan bu konfigürasyonda tüm servislerin IoC container’a aşağıdaki gibi eklenmesi gerektiğini unutmuyoruz.
builder.Services.AddSingleton<IOrderRepository, OrderRepository>(); builder.Services.AddSingleton<IProductRepository, ProductRepository>(); builder.Services.AddSingleton<IRoleManager, RoleManager>(); builder.Services.AddSingleton<IUserManager, UserManager>(); builder.Services.AddSingleton<IUserRepository, UserRepository>();
Haliyle geriye bir tek ilgili arayüzlerin inject edilmesi ve servisleri oluşturacak create metotlarının tetiklenmesi kalmaktadır.
public class FooController : ControllerBase { private readonly IRepositoryFactory _repositoryFactory; private readonly IServiceFactory _serviceFactory; private readonly IProductRepository _productRepository; private readonly IOrderRepository _orderRepository; private readonly IUserRepository _userRepository; private readonly IRoleManager _roleManager; private readonly IUserManager _userManager; public FooController(IRepositoryFactory repositoryFactory, IServiceFactory serviceFactory) { _repositoryFactory = repositoryFactory; _serviceFactory = serviceFactory; _productRepository = _repositoryFactory.CreateProductRepository(); _orderRepository = _repositoryFactory.CreateOrderRepository(); _userRepository = _repositoryFactory.CreateUserRepository(); _roleManager = _serviceFactory.CreateRoleManager(); _userManager = _serviceFactory.CreateUserManager(); } . . . }
İşte Castle Windsor ile de yapılması gerekenler bunlar 🙂
Artık hangi yaklaşım ile automatic factory’i tercih edersiniz bilemem ama controller sınıflarının hacmini injection işlemleri için lüzumsuz yere şişirmeksizin constructor injection hell’e sorumluluk üstlenen servisler üzerinden çözüm getirmenin hem keyfine hem de konforuna vardınız kanaatindeyim 🙂
İlgilenenlerin faydalanması dileğiyle…
Sonraki yazılarımda görüşmek üzere…
İyi çalışmalar…
Bu makale çok kıymetli (Diğer tüm makaleleriniz gibi). Gerçekten tam DI kullanımını kavradık, aslında ne güzel bir yaklaşım derken constructor gerçekten cehenneme dönüyor. İçerik ve örnek için teşekkürler Gençay hocam.
Selam ben Entein! Hocam yapamadığım bir yer var. Ben bu sistemi kütüphanesiz yapmaya çalışıyorum.
En son tüm cqrs içerisindeki handlerları tek bir sınıfta toplayayım dedim. KulturHandler adında bir sınıfım var. Bunun içinde tüm handlerlarım olacak. Bu handleri de controllerda çağıracağım. Çağırınca da aşağıdaki gibi kullanabileceğim.
Fakat aşağıda patladım: Devamında ne yapacağımı bilemedim. AddKulturListAsync() benden request istiyor. İstediği şeyi yazıyorum gene olmuyor 🙂