Giriş
Bu makalede Asp.Net Core Dependency Injection ve servis ömürleri (servis lifetimes) hakkında konuşacağız. Ayrıca üretim (production) sürecinde nasıl kullanılacağı, nasıl işledikleri hakkında bazı ipucu ve öneriler de paylaşacağım. Servis kapsayıcısının (service container) farklı servis ömürleriyle servisleri nasıl yönettiğini ve izlediğini gösteren örnek kod ekledim. Bu makalede genel olarak Dependency Injection ile ilgili az bahsedilen konulara ve arkaplanda ne şekilde çalıştığı ile ilgili bilgilere yer verdim. Dependency Injection nedir, nasıl kullanırım diye merak ediyorsanız giriş seviyesi bir çok güzel anlatımlı makaleler var, bu konuyla ilgili öncelikli olarak Microsoft’un kendi dökümanlarını öneririm.
Asp.Net Core, ekstra bir ayar yapmanıza ihtiyaç duymadan dependency injection modelini kullanır. Diğer taraftan, bir .Net Core konsol uygulaması yazıyorsanız, Dependency Injection kullanmak için servis kapsayıcıyı kendiniz ayarlamalısınız. Bu aslında karmaşık bir işlem değil, verdiğim örnek kod ve repository (kod deposu)’de nasıl yapıldığını görebilirsiniz. Bir şablon kullanarak oluşturduğunuz yeni bir Asp.Net Core Web uygulaması, otomatik olarak Startup isimli, servis kayıtlarını ve servis ilk hazırlıklarını yapabileceğiniz bir sınıf (class) oluşturur. Çoğunlukla uygulamanızın ihtiyaç duyduğu servisleri bu sınıfın ConfigureServices metodunda tanımlarsınız.
Asp.Net Core’da varsayılan olarak Dependency Injection (DI) (Bağımlılık Enjeksiyonu) için Microsoft.Extensions.DependencyInjection kütüphanesi kullanılmaktadır. Bu kütüphane çoğu uygulamaya yeterli olacak bir çok özellik içerir. Bu kütüphanenin sunduğundan daha fazla özelliğe ihtiyaç duyarsanız, üçüncü parti kütüphaneleri direk olarak veya entegre bir şekilde (önerilen) kullanabilirsiniz. Özelleştirilmiş servis ömürleri, alt servis kapsayıcıları (child service containers) ve benzeri ekstra özelliklere ihtiyacınız yoksa varsayılan kütüphaneyi kullanmanızı tavsiye ederim, çünkü bu kütüphane hem hafif, hem performanslı hem de .Net framework içinde yer aldığı için sürekli olarak geliştirilip güncel tutulmaktadır.
Neden Dependency Injection (Bağımlılık Ekleme)?
- Servis kayıdı için bir arabirim (interface) veya soyut bir temel sınıf (abstract base class) kullanabilirsiniz. Bu yazdığınız kodun test edilebilir olmasına yardımcı olur. Ayrıca, farklı ortamlar veya uygulama ayarları için servisin farklı tanımlamalarını da (implementation) kaydedebileceğiniz için esneklik sağlar. Örnek olarak, arka uç servisinizin (backend service) kullanıcı tarafından yüklenen belgeleri depoladığını ve sunduğunu varsayalım. IDocumentStorageService (IDokumanDepolamaServisi) şeklinde depolama hizmetinizi soyutlarsanız, kendi bilgisayarınızda yerel geliştirmeniz (local development) için FileDocumentStorageService (IDosyaDokumanDepolama Servisi), birim testleri (unit tests) için MemoryDocumentStorageService (IHafizaDokumanDepolamaServisi), Azure Depolama Hizmetlerini (Azure Storage Services) kullanmak için AzureDocumentStorageService (IAzureDokumanDepolamaServisi) kullanabilirsiniz. Diğer bulut hizmeti sağlayıcılarıyla kullanmak için servisin diğer tanımlamalarını da yazabilirsiniz.
- Framework (çerçeve) içinde tanımlı olan servis kapsayıcısı (service container), bağımlı servisleri (dependent services) otomatik olarak servisin oluşturma metoduna (constructor) enjekte eder. Ayrıca, servisleri ve bunların bağımlılıklarını oluşturmaktan ve temizlenebilir (disposable) servisleri izlemekten sorumludur. Bu, bağımlı kaynakları temizlemek ve bellek sızıntılarını (memory leaks) önlemek için daha az kod yazmanıza yardımcı olacaktır.
Servis Kapsamı (Service Scope) nedir?
Servis kapsamını (service scope), kısa ömürlü bir alt kapsayıcı (child container) gibi düşünebilirsiniz. Servis kapsamında çözümlenen tüm temizlenebilir (disposable) scoped ve transient servisler, servis kapsamı ile beraber temizleneceklerdir (dispose). Asp.Net’te sunucuya gelen her istek (request) ile beraber yeni bir servis kapsamı oluşturulur, bu nedenle istek sona erdiğinde istek içinde çözümlenen tüm servisler isteğe bağlı olan servis kapsamı ile beraber temizlenir. Bu, hem izolasyon sağlar, hem de bellek sızıntılarını (memory leaks) önlemeye yardımcı olur. İzolasyon için, her istek, sadece tenant ve kullanıcı kapsamındaki verilere erişebilen servisler oluşturabilir.
Servis ömürleri nedir?
Varsayılan dependency injection (bağılılık ekleme) framework’ü (çerçeve) bize üç adet servis ömrü sunar. Bu servis ömürleri, servislerin ne şekilde çözümleneceğini ve temizleneceğini belirler.
- Transient (Geçici): Servis sağlayıcıdan, her servis talep edildiğinde yeni bir tane servis oluşturulur. Eğer servis temizlenebilir ise, servis kapsamı bu servise ait oluşturulan tüm örnekleri (instances) takip eder ve servis kapsamı sona erdiğinde hepsini temizler.
- Singleton (Tekton): Hazır bir örnek ile kaydedilmediyse, bu servislerin tek örneği oluşturulur. Eğer servis kapsayıcı tarafından örneği oluşturuldu ise, bu servisler kök kapsam (root scope) tarafından takip edilir. Bunun anlamı bu servisler kök kapsam sona ermediği sürece hayatta kalırlar. Eğer singleton servisiniz temizlenebilir ise, tanımlanmış tipi (implemented type) veya servis sağlayıcı fabrikası (service provider factory) olarak kaydetmediyseniz, hali hazırdaki bir örnekle kaydettirdiyseniz, servis kapsayıcı bu servisi takip edip temizlemez. Bu durumda servis kapsayıcısı sona erdiğinde manuel olarak temizlemelisiniz.
- Scoped (Kapsamlı): Her service kapsamı için yeni bir örnek oluşturulur. Bu servisler, servis kapsamı içinde singleton (tekton) gibi davranırlar. Eğer servis temizlenebilir ise, servis kapsamı sona erip ortadan kaldırıldığında otomatik olarak temizlenir.
Örnek KodÖrnek repository burada
bulabilirsiniz.
https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-000-servicelifetimes-cs
Yukarıdaki örnek koddan oluşan çıktı
Dikkat ederseniz yukarıdaki örnek kodda her servis için oluşturma ve temizleme evrelerini raporluyoruz. Ayrıca servisin mevcut bir örneğinin mi yoksa yeni bir örneğinin mi oluşturulduğundan emin olmak için SayHello metodunu çağırıyoruz.
Temizlenebilir Servisler (Disposable Services)
Bir servis IDisposable ve/veya IAsyncDisposable arabirimini (interface) tanımlıyorsa temizlenebilir kabul edilmektedir.
Bir singleton servisi temizlenebilir ise ve hazır bir örnek ile kaydedildiyse, bu servis, servis kapsayıcısı tarafından takip edilip temizlenmez. Genellikle kök servis kapsayıcısı, uygulama sona ererken ortadan kaldırıldığı için bu sorun olmayabilir. Ancak hal böyle değilse, service kapsayıcısı ortadan kaldırıldıktan sonra manuel olarak servisi temizlemez iseniz, bellek sızıntısı oluşacaktır. Lütfen aşağıdaki kodu inceleyiniz.
https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-010-servicelifetime-singleton-tracking-cs
Eğer transient servisiniz temizlenebilir ise, bu servisleri kök kapsam dışındaki kapsamlarda oluşturmalısınız. Kök kapsamlar genellikle uygulama sona erdiğinde ortadan kaldırıldığı için, temizlenebilir transient servisinizi kök kapsamda çözümlerseniz, bu servisin oluşturulan her örneği uygulama süresince hayatta kalacaklardır. Bu da sürekli olarak hafızada yer kaplayacakları anlamına gelir ve bellek sızıntısı oluşturabilirler. Temizlenebilir transient servisler için önerilen oluşturma yöntemi, bir servis fabrikası (service factory) kullanmaktır. Eğer transient servisinizin diğer servislere bağımlılığı varsa, service fabrika metoduna mevcut servis sağlayıcısını (service provider) referans olarak gönderebilirsiniz. Bu yöntemi kullandığınızda, service kapsayıcısı temizlenebilir transient servisleri takip etmeyecektir, bu durumda servise artık ihtiyaç olmadığınızda, servisi temizlemek sizin sorumluluğunuzdadır.
Örnek Kod;
https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-020-servicelifetime-disposable-services-cs
Servis Kapsamları temizlenebilir servisleri nasıl takip eder?
Aşaığıdaki şemalarda Kök Kapsam “Root Scope”, İstek “Request”, Kapsam “Scope” terimlerine karşılık gelmektedir.
Asp.Net Core’da her istekte yeni bir servis kapsamı oluşturulur. İstek, yanıt döndürüp veya hata ile karşılaşıp sona erdiğinde beraberinde oluşturulan servis kapsamı ve servis kapsamı ile beraber çözümlenip temizlenmek üzere takip edilen tüm servisler temizlenir.
Servis kapsamından…
* Bir scoped servis talep edildiğinde;
* Servis kapsamı, servisin zaten servis kapsamı içinde bir örneği yoksa yeni bir örneğini oluşturur.
* Servis kapsamı scoped servisleri her zaman takip eder.
* Bir transient servis talep edildiğinde;
* Servis kapsamı, her zaman servisin yeni bir örneğini oluşturur.
* Servis kapsamı, sadece temizlenebilir transient servisleri takip eder.
* Bir singleton service talep edildiğinde;
* Eğer servisin henüz oluşturulmuş bir örneği yoksa, kök kapsam servisin bir örneğini oluşturur.
* Kök kapsam, kapsayıcı tarafından oluşturulmuş singleton servisleri her zaman takip eder.
Servisler Nasıl Oluşturulur?
Tüm servis örnekleri talep üzerine oluşturulur, bu nedenle servis kapsayıcısına, çok fazla sayıda farklı singleton servis kaydetmiş bile olsanız, bu servisleri hali hazırda oluşturulmuş bir örnek ile kaydetmediyseniz, sadece gerektiğinde oluşturulurlar. Bu, kaydetme şeklinize göre uygulamanızın başlatılırken geçen süreyi ve kullanılan kaynakları etkiler.
Eğer servisinizi bir servis tanımlama tipi (service implementation type) ile kaydederseniz;
* Servisiniz ilk talep edildiğinde reflection (yansıma) kullanılarak oluşturulur.
* Servisiniz tekrar talep edildiğinde, eğer bir singleton servis değilse, yine reflection kullanılarak oluşturulur, ancak servis motoru ikinci talepten sonra arka planda bir servis oluşturma fabrikası (service creation factory) derler.
* Servis fabrikası derlemesinden sonra, servis yeniden talep edildiğinde bu servis fabrikası kullanılarak oluşturulur ve bu noktadan sonra servisin yeni örneklerini oluşturmak oldukça hızlı olacaktır. Eğer servis, arka planda servis fabrikası derlenir iken tekrar talep edilirse, henüz servis fabrikası hazır olmadığı için 2 kereden fazla reflection ile oluşturulabilir.
* Bu yöntem aslında bir çok framework tarafından kullanılmaktadır, çünkü genellikle servis fabrikası derlemek, Activator.CreateInstance kullanarak reflection ile oluşturmaktan daha uzun sürer. Servis, singleton ise bir kereden fazla örneği oluşturulmayacağı için hiç derleme olmaz. Eğer bir scoped veya transient servis bir kereden fazla oluşturulmadıysa, bu yöntem sayesinde gereksiz derleme önlenmiş olur. Ayrıca servis ilk talep edildiğinde önceden derleme yapılsaydı, uygulamanın başlama sürelerini uzatırdı.
* Çoğu zaman bu yöntem işe yarar. Uygulamanın başlangıçtaki ilk ısınma süresi (warm-up time) uygulama performansını etkiliyor ise, servisinizi kaydettirirken bir servis fabrikası kullanmayı göz önüne alabilirsiniz. Bu şekilde reflection ve derleme olmayacağı için servis örneği oluşturma daha hızlı olacaktır.
https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-030-servicelifetime-transientservice-registration-cs
Servisleriniz için ömür belirleme
Bağlam (context) veya fonksiyonelite ile
- Singleton kullan;
- Servisiniz, önbellekleme (cache) servisleri gibi paylaşılan bir duruma (state) sahip ise. Sabit olmayan, değiştirilebilir bir duruma (mutable state) sahip singleton servisleri thread safety (iş parçacıkları arasında güvenli bir erişim) için bir kilitleme mekanizması kullanmayı düşünmelidir.
- Servisiniz durumsuz (staless) ise. Eğer servis implementasyonunuz, oldukça hafif ve nadir kullanılıyorsa, transient bir servis olarak kaydettirmeyi de göz önüne almalısınız.
- Scoped kullan;
- Servisinizin istek süresince bir singleton gibi tek bir örneğinin olmamasını istiyorsanız. Asp.Net Core’da her istek kendi servis kapsamına sahiptir. Veritabanı ve repository servisleri genellikle scoped servis olarak kaydedilirler. EntityFramework Core‘daki DbContext te varsayılanda bir scoped servis olarak kaydedilir. Scoped servis ömrü, istek süresince çözümlenmiş tüm servislerin aynı DbContext örneğini kullanmasını sağlar.
- Transient kullan;
- Servisiniz, yürütme bağlamı (execution context) içinde özel (paylaşılmayan) bir duruma sahipse.
- Servisiniz aynı anda birden fazla iş parçacığı (thread) tarafından kullanılacaksa ve iş parçacığı için erişim güvenli değilse (not thread safe).
- Servisiniz, HttpClient gibi transient ve kısa ömürlü olması gereken bir bağımlılığa sahip ise.
Bağımlılık ile
- Singleton servisler diğer singleton servislere bağımlı olabilir. Singleton servisler transient servisleri de bağımlılık olarak kullanabilir ancak farkında olunması gereken nokta şudur ki, bu bağımlı olunan transient servisler de singleton servisler hayatta olduğu sürece yaşar. Bu da genellikle uygulamanın ömrü ile aynıdır.
- Singleton servisleri, en iyi uygulama olarak (best practices), scoped servisleri bağımlılık olarak kullanmamalıdır. Çünkü bu şekilde scoped servis bir singleton gibi davranır, ve mimari anlamında genellikle istenen bu değildir. Scoped servisler diğer scoped servisleri ve singleton servisleri bağımlılık olarak kullanabilir. Bu servisler aynı zamanda transient servisleri de bağımlılık olarak kullanabilir, ancak bu durumda tasarımınızı gözden geçirip, bu kullanmak istediğiniz transient servisi scoped bir servis olarak kaydedip kaydemeyeceğinizi değerlendirmenizi öneririm.
- Transient servisler tüm servisleri bağımlılık olarak kullabilirler. Bu servisler adından da anlaşılacağı üzere çoğunlukla kısa ömürlü geçici servislerdir.
- Bir singleton veya scoped servisin bağımlılığı olarak transient servis kullanmak isterseniz, bu servis için bir servis fabrikası kullanmayı göz önünde bulundurmalısınız. Transient servisiniz temizlenebilir ise, servis fabrikası kullanmanızı öneririm.
Faydalı Öneriler
- Kök kapsamında oluşturulan scoped servisler, temel olarak tek örnekli olur ve singleton servis gibi davranırlar, çünkü kök kapsamının ömrü boyunca takip edilip temizlenmezler. Scoped servisin bağımlılıkları varsa bu servisler de yine kök kapsamı içinde çözümlenirler. Bir scoped servisi kök kapsamdan çözümlediğinizde, eğer bu servisin temizlenebilir bir transient servise bağımlılığı varsa, transient servis te benzer şekilde kök kapsam tarafından takip edilip, kök kapsamının ömrü boyunca temizlenmeyecektir.
https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-040-servicelifetime-scopedservice-correctresolution-cs
- Diğer bir kötü tasarım ise bir singleton servisin, bir scoped servisi bağımlılık olarak kullanmasıdır. Bu tür yanlış servis referanslarını önlemek için servis kapsayıcıyı derlerken validateScopes parametresini kullanabilirsiniz. Asp.Net Core tarafından kullanılan kök servis kapsayıcı bu parametreyi true olarak kullanmaktadır.
https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-050-servicelifetime-scopevalidation-cs
- Mümkün olduğunca servis bulucu deseni (service locator pattern) kullanmaktan kaçının. Ayrıca, IServiceProvider‘ın GetService metodunu manuel kullanmaktansa dependency injection (otomatik bağımlılık belirleme) kullanın. Bu, daha kolay test etmenizi, bakım yapmanızı ve okunabilir koda sahip olmanızı sağlar.
Bunu yapmaktan kaçının
https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-060-servicelifetime-avoid-manualresolution-cs
Servis sağlayıcıya statik olarak erişmekten kaçının
https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-070-servicelifetime-avoid-servicelocator-pattern-cs
Daha iyi bir yöntem
https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-080-servicelifetime-use-autoresolution-cs
- Mvc denetleyicinizin (Mvc Controller) birden fazla uç noktası (endpoint) varsa ve bir veya daha fazla uç noktanız, diğer uç noktalar tarafından ortak kullanılmayan belirli bir servisi kullanıyorsa, Asp.Net Core Mvc, FromServicesAttribute özniteliği (attribute) sayesinde özel bir bağlama (custom binding) sağlar. Servis parametrenizde bu özniteliği kullanırsanız, Mvc servisleri, eylemi (action) çağırmadan önce servisi otomatik olarak çözümler ve metod parametresine bağlar.
Örnek kod;
https://gist.github.com/fatihmemis/bd1e9853a3b454788395a8921596d974#file-090-servicelifetime-controller-fromservicesattribute-cs
İlkler
Bu makaleyi yayınlayabilmek için, aşağıdaki öğeleri ilk defa yaptım.
* Bu benim genele açık ilk makalem. Dökümantasyon, bilgi tabanı (knowledge base), firma içi paylaşım ve eğitim amaçlı daha önce makale yazdım ancak bu benim genele açık yayınladığım ilk makalem. Şu anda çalıştığım PEAKUP firmasına, boş zamanlarımda yaptıklarımı fikri mülkiyet olarak etiketlemek yerine, beni topluluğa (community) paylaşmaya ve katkıda bulunmaya teşvik ettikleri için minnettarım. Onlar da aynı benim benim gibi topluluğa katkıda bulunmanın oldukça önemli olduğunu düşünmekteler.
* Azure Static Web Apps servislerini ilk defa kullandım. Azure Static Web Apps Azure’a eklenmiş yeni yönetilen (managed) servislerden biri ve daha önce denemeye fırsatım olmadı. Ben şu sıralar uygulamaları yayınlamak için genellikle Azure Kubernetes Servisleri, Azure App Servisleri ve Function Apps’leri kullanıyorum.
* İlk defa Docker Hub’a public 2 adet docker imajı paylaştım. Üretim için çoğunlukla private container registries (özel kapsayıcı kayıt defterleri), özellikle Azure Container Registry kullanıyorum.
* İlk defa Git Actions kullandım. Geliştirdiğimiz kurumsal uygulamalar için özel kod depoları (repositories) ve Azure DevOps Pipeline‘ları kullanıyoruz. GitHub tarafından sunulan, Git Actions’ı uzun zamandır denemeyi düşünüyordum ve sonunda, hedefime ulaşmak için kullandığım bir adımın bir parçası olarak başardım.
* İlk defa özel (custom) bir git action geliştirmem gerekti. Yani.. tek yaptığım mevcut bir repository’yi fork edip üzerinde düzenlemeler yapmaktı, ama yine de sayılır değil mi?
* Vue.js’i üretim (production) için ilk defa kullandım. Bu basit bir proje ama yine de, daha önce üretim için hiç kullanmadığınız bir teknoloji ile üretilmiş ve başkalarının kullanımına sunulmuş bir uygulama oldukça önemli bir konu. Ayrıca halihazırda oluşturulan kod üzerinde, bana uygun bir hale getirmek için bir çok düzenleme yapmış olmam da oldukça heyecan verici.
Krediler
- Kapak resmi
- Fotoğraf Hans-Peter Gauster tarafından Unsplash üzerinde