ASP.NET Core Dependency Injection
Merhaba,
Bu yazıda, son yılların popüler teknik terimlerinden olan Dependency Injection kavramını
ASP.NET Core üzerinde detaylı örneklerle anlatmaya çalışacağım.
Makale İçeriği
- Loose Coupling yaklaşımı ile refactoring
- DI Constructor Injection
- DI Container ile Service registration
- Dependecy Injection
- Inversion Of Control ( IoC )
- Dependency Inversion Principle
- xUnit Test senaryosu
Dependency Injection Nedir ?
Kısaca DI olarak adlandırdığımız bu yapı NetCore framework’ün temel ilkelerinden birisidir. Türkçemize “Bağımlılık Enjeksiyonu” adıyla çevrilmiştir.
Asıl anlatılmak istenen ise; farklı nesnelerin birbirleriyle olan bağlılıklarını daha esnek bir yapıda ele alıp oluşabilecek yazılım maliyeti ve karmaşanın önüne geçmektir.
Dependency Injection tasarım deseni iki temel prensip üzerine kurulmuştur.
- Inversion Of Control ( IoC )
- Dependency Inversion Principle
Inversion Of Control ( IoC ) ; Temel olarak uygulamanızda nesne oluşturma aşamalarının sizin yerinize framework’ün yönettiği yapıdır. IoC genel bir kavramdır ve nesnelerin farklı tekniklerle Inject edilebilmelerine olanak sağlar.
Dependency Injection ise IoC’nin uygulama yöntemlerinden biridir. IoC tarafında ayaklandırılmış nesnenin nereye inject edileceğine nokta atışı karar verilen yapıdır.
Neden Dependency Injection Kullanılır ?
Problem1 : Bağlılık Yapısı
Problem2 : Test Edilebilirlik
Proje büyüdükçe maliyet ve karmaşa artar bu nedenle yapılar olabildiğince sade, okunaklı ve anlaşılabilir hazırlanmalıdır. Yaşam döngüsü boyunca bir çok geliştirme hatta yapısal değişikliğe uğrayan projelerde kurduğumuz yapıların değişikliklerden minimum ölçüde etkilenmesi gerekir. Bahsedilen bu hedefi yakalayabilmek için loosely coupled ( Esnek bağlılık ) yaklaşımını sergileyebilmek önemli, işte tam bu noktada Dependency Injection pattern ihtiyaça yönelik yaklaşımı sunar.
Ek olarak uzun vadede Bug Fix maliyetlerinin azaltılmasında önemli rol oynayan test senaryolarının doğru ve efektif yazılabilmesi için de önemli ölçüde DI ihtiyacı vardır.
Problemin Tanımlanması
Örnek Senaryo ( iş kuralları için kodlama yapılmamıştır)
Index sayfasında Ziyaretçiler için Ürün önermesi ( ProductSuggestion ) yapılacak.
Ürün önerisinde bulunacak sınıfı aşağıdaki gibi hazırladım.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class GuestProductSuggestion { public ProductSuggestionResponse GetProductSuggestion() { // logic return new ProductSuggestionResponse { Message = "Guest Suggestion", ProductList = new List<string> { "Klavye", "Kulaklık" } }; } } |
HomeController/Index
Anasayfada ürün önerimizi gösterebilmek için Index action methodunu tanımlayalım
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public class HomeController : Controller { public IActionResult Index() { var guestProductSuggestion = new GuestProductSuggestion(); var suggestionResponse = guestProductSuggestion.GetProductSuggestion(); return View(new IndexModel { ProductSuggestion = suggestionResponse }); } } |
Yukarıdaki kod blogunu incelediğimizde HotelController sınıfının Index methodunda GuestProductSuggestion sınıfını new keyword ile oluşturduğunu görüyoruz. HomeController GuestProductSuggestion sınıfından instance almayla sorumlu olduğuna göre aralarında tam bağımlılık oluşmuş durumdadır.
Örnek Senaryo için Ek Geliştirme ; Index sayfasında Kayıtlı kullanıcı için farklı iş kuralları ve ürün önermesi oluşturulmalıdır.
Örnek senaryomuza yeni bir gelitşirme eklendi, kayıtlı kullanıcılar için farklı ürün önermeleri yapılacak. Bahsedilen kullanıcılarr için öneri dönüşü yapan yeni bir sınıf oluşturmamız gerekmektedir.
( Eğer bu aşamada soyutlama yerine if koşullandırması veya parametre ile ayrıştırma yoluna giderseniz ilerde sizi iyi şeyler beklemiyor ) 🙂 >>>>
Aşağıdaki gibi UserProductSuggestion sınıfını oluşturalım.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class UserProductSuggestion { public ProductSuggestionResponse GetProductSuggestion() { // logic return new ProductSuggestionResponse { Message = "User Suggestion", ProductList = new List<string> { "4K Android TV", "MAC Book PRO" } }; } } |
HomeController Index methodununu hatırlayalım. Kayıtlı kullanıcılar için geçerli bir tahminleme yapmak istiyorsak aşağıdaki kod bloğunu değiştirmeniz gerekiyor ve bu değişiklik farklı kod bloklarını da etkileyebilir.. İşte bu karmaşa bizim ilk problemimiz bkz;
[ Problem1: Bağlılık Yapısı ]
1 2 3 4 5 6 7 8 9 |
// var guestProductSuggestion = new GuestProductSuggestion(); // var suggestionResponse = guestProductSuggestion.GetProductSuggestion(); var userProductSuggestion = new UserProductSuggestion(); var suggestionResponse = userProductSuggestion.GetProductSuggestion(); |
Şimdi ikinci problemi tanıyalım, Ürün önerme Testleri için 2 farklı senaryo hazırladım. ( Test işlemleri için xUnit kullanılmıştır )
1. Test senaryosu ; ürün önerisinin olduğu senaryoyu test ediyor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[Fact] public void IndexExpected_AvailableGuestProductSuggestion() { var controller = new HomeController(); var actionMethod = controller.Index(); var actionResultView = Assert.IsType<ViewResult>(actionMethod); var resultModel = Assert.IsAssignableFrom<IndexModel>(actionResultView.Model); var availableProductSuggestion = resultModel.ProductSuggestion.ProductList.Count > 0; Assert.True(availableProductSuggestion); } |
2. Test senaryosu ; Herhangi bir ürün önerisinin olmadığı senaryoyu test ediyor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[Fact] public void IndexExpected_NOT_AvailableGuestProductSuggestion() { var controller = new HomeController(); var actionMethod = controller.Index(); var actionResultView = Assert.IsType<ViewResult>(actionMethod); var resultModel = Assert.IsAssignableFrom<IndexModel>(actionResultView.Model); var NotAvailableProductSuggestion = resultModel.ProductSuggestion.ProductList.Count == 0; Assert.True(NotAvailableProductSuggestion); } |
Testleri çalıştırdığımızda 1. Test senaryosunun başarılı olduğunu görüyoruz. 2. Test ise hata veriyor.
Methodu biraz daha açıklamaya çalışayım ;
- Home Controller üzerinden instance oluşturuldu.
- Index methodu Invoke edildi.
- Methodunda sonucu return değerine ulaşıldı. [ Core.DI.Model.IndexModel ]
- Model dönüşü üzerinden test result oluşturuluyor
Ürün önerisinin başarılı olmadığı test senaryosunu inceleyelim;
Hatırlatma için Index action kodlarını ekledim. GuestProductSuggestion nesnesinden instance almak ve yönetmekten sorumlu olduğundan HomeController > Index ile GuestProductSuggestion arasında tam bağımlılık vardı
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public IActionResult Index() { var guestProductSuggestion = new GuestProductSuggestion(); var suggestionResponse = guestProductSuggestion.GetProductSuggestion(); return View(new IndexModel { ProductSuggestion = suggestionResponse }); } |
Tam bağımlılık nedeniyle test için uygulanan senaryoların tamamında GetProductSuggestion methodu aynı değeri döndürecektir. ( ProductList = new List<string> { “Klavye”, “Kulaklık” } ) işte bu durum bizim ikinci problemimizi ortaya koyuyor bkz;
[ Problem2 : Test Edilebilirlik ]
Çözümü Tasarlayalım
Problemi tanımlayabildiğimize göre artık çözüm yoluna geçebiliriz. Kod tasarımında bazı değişiklikler yapmalıyız. Loose Coupling yaklaşımı ile tam bağımlılıkları ortadan kaldıracağız. Yüksek seviyedeki sınıfların düşük seviyedeki sınıflara olan bağlılığını ters bağlılığa çevireceğiz. Bunun için soyutlaştırma kullanacağız ve constructor Injection dan yararlanacağız. Inversion Of Control ile dışarıdan Inject edilen sınıfların instance yönetimini uygulamamızdan çıkartıp, üst seviyede ele alacağız. ( Service Registration )
Dependency Injection Tersine Bağlılık
Dependency Inversion Principle; Yüksek seviyeli sınıfların düşük seviyeli sınıflara direk bağımlı olmadan yönetilmesini hedefleyen prensiptir. Problemin karşılaşıldığı yapılarda bağımlılığın soyutlandırma ile tersine çevirilmesi şeklinde yorumlanabilir.
Problem1: Bağlılık Yapısı
Öncelikle HomeController sınıfının GuestProductSuggestion sınıfına bağımlılığını ortadan kaldıralım.
Soyutlandırma yönteminden faydalanacağımız için GuestProductSuggestion sınıfını Interface ile dışarıya açmamız gerekiyor.
IProductSuggestion inteface’ini oluşturalım.
1 2 3 4 5 6 7 8 |
public interface IProductSuggestion { ProductSuggestionResponse GetProductSuggestion(); } |
Ürün önerisini yapacak olan sınıflarrımızı IProductSuggestion Interface’inden türetelim.
1 2 3 4 5 6 7 |
public class UserProductSuggestion : IProductSuggestion public class GuestProductSuggestion : IProductSuggestion |
Artık HomeController’ı bağlılıktan kurtarmanın vakti geldi.Dependency Injection ile HomeController’a IProductSuggestion’ı inject edelim ve Index Action’ı düzenleyelim.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
private readonly IProductSuggestion _productSuggestion; public HomeController(IProductSuggestion productSuggestion) { _productSuggestion = productSuggestion; } public IActionResult Index() { var suggestionResponse = _productSuggestion.GetProductSuggestion(); return View(new IndexModel { ProductSuggestion = suggestionResponse }); } |
Index action methoduna bakacak olursak new ile Instance almadığımızı Dependency Injection üzerinden Constructor Injection ile gelen değeri kullandığımızı görebilirsiniz.
Inversion Of Control ile IProductSuggestion interface’imizi uygulamanın kullanabilmesi için hazır bir instance haline getirelim.
Startup.cs > ConfigureServices
1 2 3 4 5 6 7 8 |
public void ConfigureServices(IServiceCollection services) { services.AddTransient<IProductSuggestion, GuestProductSuggestion>(); } |
Problem2: Test Edilebilirlik
Test için yazdığımız kodlarda ufak değişiklikler yapacağız. Daha önce hata veren test methodumuzu aşağıdaki gibi değiştirdim.
Bağlılığımız dışarıdan Inject edildiği için Interface üzerinde Mock ile Controller test senaryoma uygun örneği oluşturabiliyorum.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public void IndexExpected_NOT_AvailableProductSuggestion() { <strong> var mockHomeController = new Mock<IProductSuggestion>(); mockHomeController.Setup(p => p.GetProductSuggestion()) .Returns(new ProductSuggestionResponse() { Message = "Guest Suggestion", ProductList = new List<string>() });</strong> var controller = new HomeController(mockHomeController.Object); var actionMethod = controller.Index(); var actionResultView = Assert.IsType<ViewResult>(actionMethod); var resultModel = Assert.IsAssignableFrom<IndexModel>(actionResultView.Model); var NotAvailableProductSuggestion = resultModel.ProductSuggestion.ProductList.Count == 0; Assert.True(NotAvailableProductSuggestion); } |
Umarım faydalı bir yazı olmuştur.
Inversion Of Control Discussion
Inversion Of Control Matin Fowler Article
Merhaba Burak,
Yazilarini buyuk keyifle takip ediyorum. Bu ise yeni baslayan genc arkadaslara yonlendirme konusunda Turkce kaynak sikintisini terimleri yok etmeden gidermek bence buyuk emek! Konunun anlatimiyla ilgili nacizane elestirim olacak. DIP genel itibariyle o kadar yuzeysel ve Dependency Injection ve IoC ile karistirilarak anlatiliyor ki bu prensipin asil noktasini kacirdigimizi dusunuyorum. Bu yazida da bu fark anlatilmamis. Giris seviyesindeki bir yazi icin ne kadar onemli ya da karisikliga sebep olur mu bilemiyorum ama DIP konusu gectiginde bunun altinin cizilmesi gerektigini dusunuyorum.
Yukarida yazdiklarim kesinlikle yazinin icerdigi yararli bilgiyi es gecme cabasi degildir. Yazi basit orneklerle acik sekilde DI ve loosly-coupled yaklasimlarini tarifliyor. Emegine saglik kardesim, yaptigin seyi imrenerek takip ediyorum.
Keep Safe!
Ilker
Selam İlker,
Öncelikle bu güzel yorumun ve eleştirin için teşekkür ederim. Senden bu yorumu ve eleştiriyi almak beni ayrıca memnun etti. Aktarmaya çalıştığım konunun bir çok örneğinde DI ve IoC kavramlarının çok karışık ve iç içe anlatıldığını ön görüp bir nebze bu iki kavramı yazıda ayrıştırmayı hedeflemiştim. DIP için söylediklerine tamamen katılıyorum. Malesef konudan çok kopmamak için bir iki cümleyle tanımını yapmak ve
çok kısa uygulanabilirliğini örnekleme yolunu seçtim.
En kısa zamanda SOLID çerçevesinde DIP ile ilgili daha detaylı bir yazı hazırlayıp makale içerisine ekleyeceğim.