Bilindiği üzere .NET, uygulamanın kendi ihtiyaçlarını karşılayacak düzeyde oldukça sade bir DI container yer alır. Bu built-in container Microsoft.Extensions.DependencyInjection NuGet paketinin içinde bulunur ve yalnızca temel özellikleri sunar.
Elbette daha gelişmiş senaryolar için kullanılabilecek birçok farklı 3. parti .NET DI kütüphanesi de mevcut. Örneğin Autofac, Castle Windsor, Ninject gibi. Bunları kullanmanın farklı avantajları bulunabiliyor. Bunlar daha gelişmişi.
Autofac veya Castle Windsor gibi gelişmiş DI container paketlerini kullanan bir geliştirici, interception, aspect-oriented programming (AOP) ve yaşam döngüsü yönetimi gibi gelişmiş özelliklere sahip olduğundan, özellikle method çağrılarını yakalama, loglama, yetkilendirme veya performans ölçümü gibi işlemleri araya girerek otomatikleştirmeyi çok kolaylaştırırlar. Yani yetkinlik listesi geniştir.
Bu container paketlerinin ortak ve çokça tercih edilen özelliklerinden biri de assembly scan yaparak servisleri otomatik olarak kaydedebilmesidir. Yani projedeki sınıflara bakarak belirli kurallara uyanları DI sistemine ekleyebilir. Böylece Startup.ConfigureServices içinde yazmak zorunda olduğunuz tekrar eden kod miktarı ciddi oranda azalır.
.NET built-in container ile 3. parti bir uygulama kullanmadan da assembly can kullanılabilir mi? Bu mümkün.
Scrutor Nedir?
Scrutor başlı başına yeni bir DI container değildir. Aslında tamamen .NET’in kendi DI container sistemini (built-in container) kullanır. Bu durum bir uygulama geliştiricisi olarak Scrutor kullanımını sizin için hem avantajlar hem de bazı sınırlamalar getirir.
Mevcut bir .NET uygulamasına eklemesi son derece kolaydır çünkü bunu kullanan bir projeye Scrutor eklemek neredeyse zahmetsizdir. Aynı DI altyapısını kullandığı için servis çözümleme (service resolution) tarafında beklenmedik davranışlarla karşılaşmanız pek olası değildir.
Scrutor yerleşik container üzerinde çalıştığı için aynı projede 3. parti bir DI kütüphanesi (Autofac, Castle Windsor, Lamar gibi) varsa aynı projede teorik olarak birlikte kullanabilirsiniz. Tabi normal bir senaryoda buna ihtiyaç olmaz, ikisinden biri tercih edilir. Bu çok yaygın bir senaryo olmasa da geçiş sürecini kolaylaştırabilir.
.NET ekibi DI altyapısında ileride kırıcı bir değişiklik yapsa bile, Scrutor doğrudan bu container yapısını kullandığı için bundan etkilenme ihtimali azdır. Alternatif container yapıları kendi implementasyonlarını taşıdığı için uyumluluk riskleri daha yüksektir. Kodlarda refactor gerekebilir.
Scrutor’un kabiliyetleri, .NET DI container yapısının sunduğu özelliklerle sınırlıdır çünkü built-in container minimal tutulmuştur ve kapsamlı özellikler eklenmesi pek beklenmez.
Scrutor Assembly Taraması
Scrutor API, IServiceCollection üzerinde çalışan iki temel extension method içerir: Scan() ve Decorate(). Burada Scan() metoduna ve onun sunduğu yapılandırma seçeneklerine değineceğim.
Scan() metodu tek bir parametre alır:
Selector: Hangi sınıfların (concrete types) taranıp kaydedileceğini belirler.
Registration Strategy: Aynı servisin birden fazla kez bulunması durumunda nasıl davranılacağını tanımlar.
Services: Her sınıfın hangi interface’ler veya kendisi olarak kaydedileceğini belirler.
Lifetime: Kaydedilen servislerin ömrünü belirler (Transient, Scoped, Singleton vb.).
Örneğin, çağrıyı yapan assembly içindeki tüm somut sınıfları tarayıp bunları Transient olarak kaydeden bir yapı şöyle görünür:
services.Scan(scan => scan
// Taranacak sınıflar bulunur
.FromCallingAssembly()
// Concrete class seçilir
.AddClasses()
// Conflict (çalışma) durumu belirleniyor
.UsingRegistrationStrategy(RegistrationStrategy.Skip)
// Kendileri olarak kaydet
.AsSelf()
// Yaşam döngüleri belirleniyor
.WithTransientLifetime());Gerçek bir örnek ile denemeler yapalım. Projemizde birçok repository, manager gibi sınıflar bulunabilir. Bunları tek tek DI container için dahil etmek uzun bir iş. Aşağıda örnek bir projemizde bulunan refactor öncesi host builder ekranını görmektesiniz.

IHost host = Host.CreateDefaultBuilder(null)
.ConfigureServices((hostContext, services) =>
{
services.AddTransient<IPriceDefinitionRepository, PriceDefinitionRepository>().BuildServiceProvider();
services.AddTransient<IMailConfigurationRepository, MailConfigurationRepository>().BuildServiceProvider();
services.AddTransient<IEmailTemplateRepository, EmailTemplateRepository>().BuildServiceProvider();
services.AddTransient<IDomainRepository, DomainRepository>().BuildServiceProvider();
services.AddTransient<IDomainInfoRepository, DomainInfoRepository>().BuildServiceProvider();
}Yüzlerce olabilir. Neden çalışan veya çağırdığım bir assembly içinden otomatik bu işlemi yapmıyor da tek tek kayıt ediyoruz? İşte burada ciddi bir kolaylık var: Assembly scanning.
Assembly taraması yaparken ilk adım, uygulamanızda hangi somut (concrete) sınıfların DI container’a kaydedileceğini belirlemektir. Scrutor bize bu türleri arama konusunda esnek yöntemler sunar. Bu yöntemlerin büyük çoğunluğu assembly tarayarak sınıfları bulmaya ve ardından listeyi belirli kurallara göre filtrelemeye yöneliktir.
Geleneksel yöntem ile DI container’a sınıflar böyle kaydedilir:
services.AddTransient<Service1>();
services.AddTransient<Service2>();Scrutor ile şöyle yapabilirsiniz:
services.Scan(scan => scan
.AddTypes<Service1, Service2>()
.AsSelf()
.WithTransientLifetime());Scrutor’un en güçlü olduğu nokta, assembly tarayarak tipleri otomatik kaydedebilmesidir. Scrutor bu işlem için farklı ihtiyaçlara göre birçok yöntem sunar. Bunlarla doğrudan Assembly nesnelerini belirtebilir. Uygulamanın bağımlılıklarına göre assembly listesi oluşturabilir veya çalışan assembly baz alınabilir.
Assembly tarama yöntemleri:
- FromAssemblyOf – Verilen türde tarar
- FromCallingAssembly, FromExecutingAssembly, FromEntryAssembly – Çağıran, çalışan veya uygulamanın giriş noktası olan öğelerini tarar.
- FromAssemblyDependencies – Verilen assembly içinde bağımlı olduğu tüm öğeleri tarar
- FromApplicationDependencies, FromDependencyContext – Uygulamanın runtime bağımlılıklarını tarar.
Bulunan Öğelerin Filtrelenmesi
Hangi assembly tarama yöntemini kullanırsanız kullanın, sınıfları seçmek için mutlaka AddClasses() metodunu çağırmanız gerekir. Bu metod bize container içine eklenecek concrete sınıfları seçer ve çeşitli filtreleme seçenekleri sunar:
AddClasses() sadece public ve abstract olmayan sınıfları ekler yani erişim belirteci “public” olmayan sınıfları asla dahil etmez. AddClasses(publicOnly) versiyonunda ise ‘false’ verilirse internal veya private nested sınıflar da taramaya katılır, dolayısıyla kapsam genişler. AddClasses(predicate) yöntemi, herhangi bir koşulu (interface veya attribute gibi) kullanarak daha ince filtreleme yapma olanağı sunar. Son olarak AddClasses(predicate, publicOnly) ise hem erişim görünürlüğünü hem de özel koşulları bir arada kontrol etmenizi sağlar.
Örnek bir kullanım:
services.Scan(scan => scan
.FromAssemblyOf()
.AddClasses(classes => classes.AssignableTo())
.AsImplementedInterfaces()
.WithTransientLifetime());Belirli bir namespace altındaki sınıfları seçmek isterseniz:
services.Scan(scan => scan
.FromAssemblyOf()
.AddClasses(classes => classes.InNamespaces("Reseller.Domain"))
.AsImplementedInterfaces()
.WithTransientLifetime());Farklı örnek olarak, yukarıda verdiğim “repository” DI container örneğini Scrutor ile yapalım. Amacım uygulamanın runtime bağımlılıkları içinde ismi sadece “Reseller” ile başlayan assembly öğelerini filtrelemek ve burada bulunan sınıfları kaydetmek.
services.Scan(scan => scan
.FromApplicationDependencies(a =>
a.GetName().Name.StartsWith("Reseller"))
.AddClasses(c => c.Where(t =>
t.Name.EndsWith("Repository")))
.AsImplementedInterfaces()
.WithTransientLifetime()Bakın 100 satır yazmak yerine tek satır ile bu işlemi yaptık.
Mükerrer Servisler
Scrutor size ayrıca çift kayıt edilen servisler varsa onları da yönetmenizi sağlar. Yani bir servis DI container içinde zaten kayıtlıysa bu durumu nasıl yöneteceğinizi kontrol etmenizi sağlar. Bunun için ReplacementStrategy kullanabilirsiniz. Şu anda beş farklı strateji mevcuttur:
Append: Çift kayıtları önemsemeden yeni kayıtları ekler. Eğer bir strateji belirtmezseniz bu varsayılan davranıştır.
Skip: Servis zaten kayıtlıysa yeni bir kayıt eklemez.
Replace(ReplacementBehavior.ServiceType): Interface daha evvel kayıtlıysa öncekileri silip yenilerini ekler.
Replace(ReplacementBehavior.ImplementationType): Implementation tipi daha önce kayıtlıysa eşleşen öncekileri kaldırır ve yenilerini ekler.
Replace(ReplacementBehavior.All): Hem servis türü hem implementation türü için ayn işlemi yapar.
Örnek kullanım:
services.Scan(scan => scan
.FromAssemblyOf<IService>()
.AddClasses()
.UsingRegistrationStrategy(RegistrationStrategy.Skip)
.AsSelf()
.WithTransientLifetime());
ReplacementStrategy, Scrutor’da en kafa karıştırıcı konulardan biridir. Mümkünse manuel olarak eklediğiniz sınıfları tarama (Scan) sırasında keşfedilecek şekilde eklememeye çalışarak bu stratejiyi kullanmaktan kaçınmak en güvenli yaklaşımdır. Böylece hem çakışmaları önler hem de kayıt yönetimini basitleştirirsiniz.
Bağımlılıkları Servis Olarak Kaydetmek
Scrutor ile hangi sınıfların kaydedileceğini ve kayıt stratejisini nasıl seçeceğimizi gördük. Şimdi ise DI container’da nasıl kaydedileceğini belirleyelim.
Scrutor, bir sınıfı servise dönüştürürken birçok seçenek sunar.
1- AsSelf: Sınıfı kendisi olarak kaydeder.
// Scrutor ile
services.Scan(scan => scan
.FromAssemblyOf()
.AddClasses()
.AsSelf()
.WithSingletonLifetime());
// Manuel:
services.AddSingleton();
services.AddSingleton();2- AsMatchingInterface: Sınıf ile aynı isme sahip interface varsa o interface üzerinden kaydeder. Bu en çok kullanılan olabilir.
services.Scan(scan => scan
.FromAssemblyOf()
.AddClasses()
.AsMatchingInterface()
.WithScopedLifetime());
// Manuel
services.AddSingleton<IService, Service1>();
services.AddSingleton<IService, Service2>();3- AsImplementedInterfaces: Sınıfın implement ettiği tüm interface’leri kaydeder. Böylece aynı sınıf birden fazla interface üzerinden çözümlenebilir.
services.Scan(scan => scan
.FromAssemblyOf()
.AddClasses()
.AsImplementedInterfaces()
.WithTransientLifetime());
// Manuel
services.AddTransient();
services.AddTransient();4- AsSelfWithInterfaces: Hem sınıfın kendisi hem de implement ettiği tüm interface değerleri üzerinden kaydeder.
services.Scan(scan => scan
.FromAssemblyOf()
.AddClasses()
.AsSelfWithInterfaces()
.WithScopedLifetime());
// Manuel
services.AddScoped();
services.AddScoped();
services.AddScoped();5- As: Sınıfı belirli bir servis tipi üzerinden kaydetmek için kullanılır. Örneğin tüm sınıfları sadece IService interface üzerinden çözümlemek istiyorsanız şöyle yapın:
services.Scan(scan => scan
.FromAssemblyOf()
.AddClasses()
.As()
.WithSingletonLifetime());
// Manuel
services.AddSingleton();
services.AddSingleton();Yaşam Döngülerini Belirlemek
.NET DI container için bir sınıf kaydederken mutlaka servisin yaşam döngüsünü (lifetime) belirtmek gerekir. Scrutor bize 3 seçenek sunuyor:
WithTransientLifetime: Transient yaşam döngüsü, herhangi bir lifetime belirtilmezse varsayılan olarak kullanılır.
WithScopedLifetime: Scoped yaşam döngüsü, servisi bir HTTP isteği süresince tekil kullanır. Her yeni istek için yeni bir nesne oluşturulur.
WithSingletonLifetime: Singleton yaşam döngüsü servisin uygulama boyunca tek bir instance ile paylaşılmasını sağlar.
Şimdi assembly tarama, filtreleme, kayıt stratejisi, kayıt tipi ve lifetime kullanarak Scrutor ile servislerinizi tamamen otomatik olarak DI container’a kaydedebilirsiniz.
Scrutor, [ServiceDescriptor] attribute sayesinde sınıfların DI container’a nasıl kaydedileceğini doğrudan sınıf üzerinde belirlemenize olanak tanır. Yani her sınıfın kendi içinde hangi interface üzerinden, hangi yaşam döngüsüyle ve hangi strateji ile kaydedileceğini tanımlayabilirsiniz. Bu özellikle çok sayıda sınıfın bulunduğu ve tarama sırasında farklı kurallar uygulamak istediğiniz projelerde kullanışlıdır.
Küçük bir örnekle gösterelim:
using Scrutor.Attributes;
using Microsoft.Extensions.DependencyInjection;
[ServiceDescriptor(ServiceType = typeof(IOrderService), Lifetime = ServiceLifetime.Singleton)]
public class OrderService : IOrderService
{
}
[ServiceDescriptor(ServiceType = typeof(IUserService), Lifetime = ServiceLifetime.Scoped)]
public class UserService : IUserService
{
}Sonrasında Scrutor ile Scan() yaparken bu attribute kullanımları dikkate alınır ve sınıflar DI container’a belirtilen şekilde eklenir:
services.Scan(scan => scan
.FromAssemblyOf()
.AddClasses()
.UsingAttributes());Bu yöntemle her sınıf için tek tek Scan() içinde kurallar yazmanıza gerek kalmaz. Ancak dikkat edilmesi gereken nokta şudur, [ServiceDescriptor] kullanımı biraz daha karmaşık ve attribute bazlı olduğu için büyük projelerde yönetimi zorlaşabilir. Bu sebeple attribute bazlı kullanım daha küçük ölçekli projelerde mantıklı olabilir.
Çoklu Filtre ve Kayıt Kurallarını Tek Scan() ile Yönetmek
Şimdi Scrutor ile tek bir Scan() çağrısı içinde birden fazla filtre ve kayıt kuralını zincirleme olarak uygulayalım. Yani bir assembly’deki farklı türdeki sınıfları ayrı ayrı alt kümeler halinde belirleyip her alt küme için farklı kayıt biçimi ve kuralları uygulayalım.
Bu durum kodları oldukça kısaltıyor, gereksiz “service.AddTransient” satırlarından kurtuluyoruz. Farklı sınıf tipleri için farklı kayıt yöntemleri ve lifetimeler uygulamak mümkün oluyor.
Şimdi Scrutor ile gelinen son noktada, DI kayıtlarını çok okunaklı ve merkezi bir şekilde organize edelim:
services.Scan(scan => scan
.FromApplicationDependencies(a =>
a.GetName().Name.StartsWith("Reseller"))
// Repository sınıfları - Interface ile kayıt
.AddClasses(c => c.Where(t =>
t.Name.EndsWith("Repository")))
.AsImplementedInterfaces()
.WithTransientLifetime()
// Manager sınıflar - Interface olmadan kayıt
.AddClasses(c => c.Where(t =>
t.Name.EndsWith("Manager")))
.AsSelf()
.WithTransientLifetime()
// Endpoint ve container
.AddClasses(c => c.Where(t =>
t.Name.EndsWith("Endpoint") ||
t.Name.EndsWith("Container") ||
t.Name.EndsWith("Context")))
.AsImplementedInterfaces()
.WithTransientLifetime()
);Scrutor, .NET’te kullanılan Microsoft.Extensions.DependencyInjection DI container’a assembly tarama yetenekleri ekler. Bu bir üçüncü taraf DI container değildir. Tam aksine yerleşik container’ı genişleterek servislerinizi kaydetmeyi kolaylaştırır.
Birçok projenizde kolaylık getireceğinden eminim.



Yorum bırakın