ASP.NET Core & Dependency Injection
Bu makale Yazılım Mimari Thanh Le tarafından yazılan “[.NET Core] Dependency Injection in ASP .NET Core — “Old but gold”” makalesinin Türkçe çevirisidir. Kendisine bu makaleyi Türkçeye çevirmem için izin ve destek verdiği için teşekkür ediyorum. Standart haline gelmiş bazı kelimeleri çevirmediğimi göreceksinizdir makale içerisinde, anlam bütünlüğünün ve standarta uymak açısından bazı kelimeleri çevirmeden sizlere sundum.
Çay veya kahvenizi aldıysanız, başlayalım! Keyifli okumalar…
Bu makalede Dependency Inversion Principle (DIP), Inversion of Control (IoC) ve Dependency Injection (DI) hakkında konuşacağız. Bu konuların ardından, .NET Core’un özelliklerinden yararlanarak farklı yollarla (Controller Constructor Injection, Controller Method Injection ve View Injection) basit bir örnek yaparak, Dependency Injection uygulanışı hakkında bilgi sahibi olacağız.
Not: Eğer Dependency Inversion Principle (DIP), Inversion of Control (Ioc) ve Dependency Injection (DI) konularını zaten biliyorsanız, rahatlıkla bu kısımları geçebilirsiniz.
Dependency Inversion Principle (DIP)
Dependency Inversion Prensibi, SOLID Yazılım Geliştirme Prensiplerindeki “D” prensibidir. Bu prensip 90'lı yıllarda Robert C Martin tarafından tanıtıldı. Prensibin tanıtıldığı orjinal dökümana buradan ulaşabilirsiniz.
Not: Yazının bu kısmından sonra Dependency Inversion Prensibini “DIP” olarak kısaltmakla beraber, abstract class ve concrete class gibi özel terimleri “soyut sınıf” ve “somut sınıf” olarak çevireceğim.
DIP, esnek sınıflar yazmamıza yardımcı olan bir yazılım tasarımıdır. Wiki’deki DIP tanımına göre:
● Yüksek seviyeli modüller, düşük seviyeli modüllere bağlı olmamalıdır. Her ikisi de soyutlamalara bağlı olmalıdır.
● Soyutlamalar detaylara bağlı olmamalıdır. Ayrıntılar soyutlamalara bağlı olmalıdır.
Yukarıdaki tanımları daha iyi anlamak için size bir örnek vereyim:
Bir siparişi bitirdiğinde, sistemin son kullanıcıya e-posta göndermesine izin veren bir fonksiyon yazmanız gerektiğini düşünelim. Biri sipariş için, diğeri de e-postayı göndermek için olmak üzere 2 tane sınıf(class) oluşturmalıyız.
İlk olarak, kod mantığında herhangi bir sorun yok, son kullanıcı siparişini bitirdikten sonra “Send” fonksiyonu tetiklenecektir. Ancak, bu Dependency Inversion Prensibini ihlal ediyor çünkü SendingEmail sınıfı ve ona bağlı olan Ordering sınıfı soyut değil, somut sınıflardır. Peki buradaki problem nedir? Farz edelim ki iş ekibinizden, E-posta yerine SMS kullanmak için iletişim türünü değiştirmenizi gerektiren yeni bir istek aldınız, ne olur?
Sonuç olarak, SendingSMS sınıfı oluşturmanız ve Ordering sınıfında bunun bir örneğini bildirmeniz gerekir. Ve her farklı iletişim türü eklediğimizde bunu teklarlamak zorundasınız. Bunun sonucunda ise, SMS veya E-posta’yı iletişim aracı olarak kullanmaya karar vermek için “IF ELSE” koşul ifadeleri kullanmak zorunda kalırsınız. Fakat, E-posta ve SMS’nin yanında daha fazla seçeneğiniz olduğunda bu durum gittikçe daha da kötüleşir. Bu, Ordering sınıfında daha fazla yeni sınıf tanımlamak gerektiği anlamına gelir.
Dependency Inversion Prensibi, sistemi/programı daha yüksek seviyeli modüller (bu örneğimizde Ordering sınıfı) olan soyutlamalara bağlı olacak ve somut sınıflar yerine kullanacak şekilde ayırmamız gerektiğini söylüyor. Bu soyutlama, asıl kod mantığını gerçekleştirecek olan gerçek somut sınıfıyla eşleştirilecektir. (Sonraki kısımlarda örneğini görebilirsiniz)
Inversion of Control (IoC)
Inversion of Control (IoC), alt seviye modüllerin somut uygulamasından ziyade soyutlamalara bağlı olarak daha yüksek seviye modüller yapmamıza yardımcı olan tekniktir. Başka bir deyişle, Dependency Inversion Prensibinin uygulanmasına yardımcı olur. Hadi yukarıda incelediğimiz örneğe geri dönelim ve IoC’yi uygulayalım.
İlk olarak, üst düzey Ordering sınıfının bağlı olacağı bir soyutlama yaratmamız gerekir.
Soyutlama için arayüzümüzü oluşturduktan sonra, ICustomerCommunication arayüzünden miras almaları için “SendingEmail” ve “SendingSMS” sınıfını güncelleyin.
Şimdi daha düşük seviyeli somut sınıftan ziyade, yüksek seviyeli modül, Ordering sınıfını bu soyutlamayı kullanacak şekilde değiştirelim.
Sonuç olarak, tasarım şöyle görünecektir:
Burada yaptığımız şey, DIP’ye uymak için kod işleyiş kontrolünü tersine çevirmektir. Şimdi yüksek seviyeli modüllerimiz, sadece DIP’nin belirttiği gibi daha düşük seviyeli somut uygulamalarına değil, sadece soyutlamalara bağımlıdır.
Dependency Injection (DI)
Yukarıdaki örneğimizde IoC’yi uygulayıp, Ordering sınıfını ICustomerCommunication arayüzüne/soyutlamasına bağlı hale getirdik. Ancak hala Ordering sınıfında (daha üst düzey bir modül) somut sınıflar kullanıyoruz. Bu, sınıfları tamamen birbirinden ayırmamızı engeller.
Tam burada Dependency Injection (DI) devreye giriyor!
DI temel olarak somut uygulamayı, soyutlama kullanan bir sınıfa enjekte etmek içindir (i.e. ICustomerCommunication arayüzü). DI’nin ana fikri, sınıflar arasındaki bağlantıyı azaltmak, soyutlamanın ve somut uygulamanın bağlayıcılığını bağımlı sınıfın dışına taşımaktır. DI’yi 3 şekilde uygulayabiliriz:
● Constructor Injection
● Method Injection
● Property Injection
- Constructor Injection
Bu yaklaşımda, somut sınıfın nesnesini bağımlı sınıfın yapıcı fonksiyonuna (constructor) geçireceğiz ve onu kullandığı arayüze atayacağız.
Yukarıdaki kodda, yapıcı fonksiyon (constructor), somut sınıf objesini alacak ve arayüz örneğine bağlayacak. Eğer SendingSMS’in somut örneğini bu sınıfa geçirmemiz gerekirse, tek yapmamız gereken SendingSMS sınıfının bir örneğini yaratmak ve ardından onu aşağıdaki gibi Ordering sınıfının yapıcı fonksiyonuna (constructor) parametre olarak iletmektir:
2. Method Injection
Constructor Injection kullanırken, somut sınıf (Ordering sınıfının ömrü boyunca SendingSMS veya SendingEmail sınıfı) örneğini kullanmak zorundayız. Şimdi ise somut sınıf örneğini uygulamanın her metoduna geçirmek istiyorsak Method Injection yöntemini kullanmalıyız.
Ve Method Injection yöntemini aşağıdaki gibi kullanacağız:
3. Property Injection
Artık bağımlı sınıfın Constructor Injection ile tüm yaşam döngüsü boyunca bir somut sınıf kullanacağını ve Method Injection’ın sadece “method” seviyesinde etki edeceğini biliyoruz. Ancak, ya somut sınıf örneği ve metodun çağrılmasının uygulamaları/sorumluluğu ayrı yerlerde bulunuyorsa. Bu gibi durumlarda, Property Injection kullanımına ihtiyacımız var.
Bu yaklaşımla, somut sınıfın nesnesini bağımlı sınıf tarafından ortaya çıkarılan bir setter property üzerinden geçiriyoruz/uyguluyoruz.
Ve Property Injection yöntemini aşağıdaki gibi kullanacağız:
Constructor Injection, DI’nin uygulanması söz konusu olduğunda en çok kullanılan injection yöntemidir. Eğer her method çağrısına farklı bağımlılıklar geçirmemiz gerekiyorsa, Method Injection kullanırız. Property Injection, diğer yöntemlere göre daha az kullanılır.
Şu andan itibaren, yeni başlayan biriyseniz artık DIP, IoC ve DI hakkında bilgi sahibisiniz. Bir sonraki kısımda, bir .NET Core projesi içerisine Dependency Injection (DI) implemente edeceğiz/uygulayacağız.
ASP .NET Core’da Dependency Injection
Dependency Injection, ASP.NET Core içerisinde bulunan ve desteklenen bir özelliktir. Bu destek sadece ara katmanlarla (Middlewares) sınırlı değil, ayrıca Model, View ve Controllerlar içerisinde de mevcut. ASP.NET Core tarafından sağlanan, iki tipte servis konteyneri(Service Container) bulunmaktadır: Framework Services and Application Services.
Framework Servisleri, ILogger gibi ASP.NET Core tarafından desteklenen servislerdir.
Application Servisleri ise ihtiyaçlara/gereksinimlere göre oluşturulan özel servislerdir.
Controllerda Dependency Injection
ASP.NET Core, yapıcı fonksiyon (constructor) tabanlı bağımlılığa destek vermektedir. Controller’ın gerektirdiği bağımlılık, yalnızca yapıcıdaki controllera bir servis türü eklemektir. ASP.NET Core, servis tipini tanımlar ve tipi çözümlemeye çalışır. Evet böyle tanımlamalarla aklımızda tam olarak şekillendiremiyoruz bu yüzden haydi bir örnekle anlamaya çalışalım!
İlk olarak, “IWelcomeMessage” arayüzünden miras alan “WelcomeMessage” adlı bir somut sınıf oluşturacağız (öğrendiğimiz üzere bu bir soyutlama (abstraction) yöntemidir).
Şimdi bu servisi servis konteynerine eklemeliyiz, böylece Controller servise istekte bulunabilir ve istediğinde onu kullanabilir. Servisimizi, Startup.cs sınıfının ConfigureServices metodu içerisinde tanımlayarak, servis konteynerine (Service Container) ekleyebiliriz. Bu servislerin 3 farklı yaşam opsiyonu mevcuttur: Transient, Scoped, and Singleton (Bu opsiyonlara önümüzdeki kısımlarda değineceğiz).
Son olarak, yapıcı fonksiyon (constructor) aracılığıyla servisimiz controllera enjekte edilir.
Artık sonucu görebiliriz:
Lütfen Dependency Injection’ı Startup sınıfının ConfigureServices metodu içerisine kaydettiğinizden emin olun, aksi takdirde aşağıdaki gibi bir hata ile karşılacaksınız:
Controller Metodlarına/Actionlarına Bağımlılığı Enjekte Etme — Method Injection
ASP.NET Core, “FromServices” attribute’u kullanarak belirli action’a bağımlılığı enjekte etmemizi sağlar. Bu attribute, ASP.NET Core framework’une, parametrenin servis konteynerinden (Service Container) alınması gerektiğini bildirir.
Ve sonuç hala aynı:
Not: Property Injection, ASP.NET Core tarafından desteklenmemektedir.
Servisi Manuel Olarak Enjekte Etmek
Bu metotda servis, Controller constructorına veya Controller actionına parametre olarak enjekte edilmez. “HttpContext.RequestServices” özelliğinin “GetService” metodunu kullanarak servis konteyneri ile yapılandırılmış bağımlı servisleri alabiliriz.
Servisi Viewlara Enjekte Etmek
ASP.NET Core, View bağımlılığını da enjekte edebilir. Bu, View ile ilgili “yerelleştirme(localization)” gibi bir servisi enjekte etmek için çok kullanışlıdır. @İnject direktifini kullanarak servis bağımlılığını view’e enjekte edebiliriz.
View Injection, dropdown menü gibi UI öğelerini doldurmak için kullanılabilir. Örnek vermek gerekirse ülkelerin bulunduğu bir liste servisten dropdown listesine doldurulabilir. Bu tür şeyleri servisten uygulamak ASP.NET Core’daki standart yaklaşımdır. Alternatif olarak, dropdown menüyü doldurmak için ViewBag ve ViewData’yı kullanabiliriz. @İnject direktifi, enjekte edilen servisi geçersiz kılmak (override) için de kullanılır. Örneğin, dropdown, textbox vb. gibi Html etiketlerini oluşturmak için Html helper servisini kullanıyoruz. @İnject direktifini kullanarak bu servisi kendi servisimizle değiştirebiliriz.
Service Lifetime (Servis Ömrü)
ASP.NET Core, kayıtlı servislerin ömrünü belirlememize izin verir. Servis örneği, belirtilen yaşam süresine göre otomatik olarak imha(disposed) edilir. Bu nedenle bu bağımlılığın temizlenmesini umursamıyoruz, ASP.NET Core framework’ü bizim yerimize bununla ilgilenecektir. Önceden bahsettiğimiz gibi 3 tip servis yaşam süresi bulunmaktadır.
Singleton
Uygulama, yaşam ömrü boyunca servisin tek bir örneğini(instance) oluşturur ve paylaşır. Servis, IServiceCollection’ın AddSingleton metodu kullanılarak singleton olarak eklenebilir. ASP.NET Core, kayıt sırasında servis örneği oluşturur ve sonraki istekte(request) bu hizmet örneğini kullanır.
Scoped
ASP.NET Core, uygulama için istek(request) başına servisin bir örneğini oluşturur ve paylaşır. Bu, istek başına tek bir servis örneğinin mevcut olduğu anlamına gelir. Her yeni istek için yeni bir örnek oluşturur. Servis, ConfigureServices (Startup class) içindeki IServiceCollection’ın AddScoped metodunu kullanılarak kapsam dahilinde(scoped) eklenebilir.
Transient
ASP.NET Core, istediğimiz zaman uygulamaya her seferinde bir servis örneği oluşturur ve paylaşır. Servis, IServiceCollection’ın AddTransient metodu ile Transient(geçiçi/süreksiz) olarak eklenebilir. Bu yaşam süresi stateless serviste kullanılabilir. Bu yaşam ömrü tipi, lightweight(hafif) servis eklemenin bir yoludur.
Sonuç
Dependency Injection (DI) yazılım geliştirme tasarım kalıplarından en önemlilerinden biridir. Bu, daha fazla esneklik, sürdürülebilirlik, test edilebilirlik ve tekrar kullanılabilirlik sağlaması için esnek bir şekilde uygulama oluşturmamıza yardımcı olacaktır. ASP.NET Core’da yerleşik olarak bulunması ve desteklenmesiyle beraber, kolayca uygulamamıza Dependency Injection uygulayabiliriz.