ASP.NET Çekirdek Bağımlılığı Enjeksiyonu En İyi Uygulamalar, İpuçları ve Püf Noktaları

Bu makalede, ASP.NET Core uygulamalarında Bağımlılık Enjeksiyonunu kullanma konusundaki deneyimlerimi ve önerilerimi paylaşacağım. Bu ilkelerin arkasındaki motivasyon;

  • Etkili tasarım hizmetleri ve bağımlılıkları.
  • Çok iş parçacıklı sorunları önleme.
  • Bellek sızıntılarını önleme.
  • Potansiyel hataları önleme.

Bu makalede, bağımlılık enjeksiyonu ve ASP.NET Core hakkında temel bir düzeyde bilgi sahibi olduğunuz varsayılmaktadır. Değilse, lütfen önce ASP.NET Çekirdek Bağımlılığı Enjeksiyonu belgelerini okuyun.

temeller

Yapıcı Enjeksiyon

Yapıcı enjeksiyon, bir servisin servis inşasına bağımlılıklarını bildirmek ve elde etmek için kullanılır. Örnek:

kamu sınıfı ProductService
{
    özel salt okunur IProductRepository _productRepository;
    Genel Ürün Hizmeti (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    genel boşluk Sil (int id)
    {
        _productRepository.Delete (kimlik);
    }
}

ProductService, IProductRepository'yi yapıcısına bağımlılık olarak enjekte ediyor ve sonra onu Delete yönteminin içinde kullanıyor.

İyi Uygulamalar:

  • Servis kurucuda açıkça istenen bağımlılıkları tanımlayın. Bu nedenle, hizmet bağımlılıkları olmadan inşa edilemez.
  • Enjekte edilen bağımlılığı salt okunur bir alana / özelliğe atayın (yanlışlıkla bir yöntemin kendisine başka bir değer vermesini önlemek için).

Mülkiyet Enjeksiyonu

ASP.NET Core’un standart bağımlılık enjeksiyon kabı, mal enjeksiyonunu desteklemiyor. Ancak mülk enjeksiyonunu destekleyen başka bir kap kullanabilirsiniz. Örnek:

Microsoft.Extensions.Logging kullanarak;
Microsoft.Extensions.Logging.Abstractions kullanarak;
ad alanı MyApp
{
    kamu sınıfı ProductService
    {
        genel ILogger <Ürün Hizmeti> Logger {get; Ayarlamak; }
        özel salt okunur IProductRepository _productRepository;
        Genel Ürün Hizmeti (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger <ÜrünServis> .Instance;
        }
        genel boşluk Sil (int id)
        {
            _productRepository.Delete (kimlik);
            Logger.LogInformation (
                $ "İd = {id} ile bir ürün silindi");
        }
    }
}

ProductService, genel ayarlayıcıyla bir Logger özelliği ilan ediyor. Bağımlılık enjeksiyon kabı, varsa Logger'ı ayarlayabilir (daha önce DI kabına kayıtlı).

İyi Uygulamalar:

  • Özellik enjeksiyonunu yalnızca isteğe bağlı bağımlılıklar için kullanın. Bu, verilen bu bağımlılıklar olmadan hizmetinizin düzgün çalışabileceği anlamına gelir.
  • Mümkünse Boş Nesne Kalıbı (bu örnekte olduğu gibi) kullanın. Aksi takdirde, bağımlılığı kullanırken daima boş değeri kontrol edin.

Servis Bulucu

Servis bulma modeli, bağımlılık kazanmanın başka bir yoludur. Örnek:

kamu sınıfı ProductService
{
    özel salt okunur IProductRepository _productRepository;
    özel salt okunur ILogger <ÜrünServis> _logger;
    genel ProductService (IServiceProvider hizmetiProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > () ??
            NullLogger  .Instance;
    }
    genel boşluk Sil (int id)
    {
        _productRepository.Delete (kimlik);
        _logger.LogInformation ($ "ID = {id}" ile bir ürün silindi);
    }
}

ProductService, IServiceProvider'ı enjekte ediyor ve bağımlılıklarını kullanıyor. İstenen bağımlılık daha önce kaydedilmemişse, GetRequiredService istisna atar. Öte yandan, GetService bu durumda null değerini döndürür.

Yapıcı içindeki hizmetleri çözdüğünüzde, hizmet verildiğinde serbest bırakılırlar. Bu nedenle, yapıcı içinde çözülen hizmetleri salıverme / elden çıkarma ile ilgilenmezsiniz (tıpkı yapıcı ve mülk enjeksiyonu gibi).

İyi Uygulamalar:

  • Servis bulma düzenini mümkün olan her yerde kullanmayın (servis tipi geliştirme zamanında biliniyorsa). Çünkü bağımlılıkları örtülü yapar. Bu, hizmetin bir örneğini oluştururken bağımlılıkları kolayca görmenin mümkün olmadığı anlamına gelir. Bu, özellikle bir servisin bazı bağımlılıklarını alay etmek isteyebileceğiniz birim testleri için önemlidir.
  • Mümkünse hizmet kurucudaki bağımlılıkları giderin. Bir servis yönteminde çözümleme, uygulamanızı daha karmaşık ve hataya açık hale getirir. Sorunları ve çözümleri sonraki bölümlerde ele alacağım.

Hizmet ömrü

ASP.NET Çekirdek Bağımlılığı Enjeksiyonunda üç servis ömrü vardır:

  1. Geçici hizmetler her enjekte edildiğinde veya talep edildiğinde oluşturulur.
  2. Kapsamlı hizmetler, kapsam başına oluşturulur. Bir web uygulamasında, her web isteği yeni bir ayrılmış servis kapsamı oluşturur. Bu, kapsamlı hizmetlerin genellikle web isteği başına yaratıldığı anlamına gelir.
  3. DI konteyner başına Singleton hizmetleri oluşturulur. Bu genellikle, uygulama başına yalnızca bir kez yaratıldıkları ve daha sonra tüm uygulama ömrü boyunca kullanıldığı anlamına gelir.

DI konteyner tüm çözümlenmiş servislerin kaydını tutar. Hizmetler ömürleri sona erdiğinde serbest bırakılır ve atılır:

  • Hizmetin bağımlılıkları varsa, otomatik olarak serbest bırakılır ve imha edilir.
  • Hizmet tanımlanabilir bir arabirim uygularsa, Dispose yöntemi otomatik olarak hizmet sürümünde çağrılır.

İyi Uygulamalar:

  • Hizmetlerinizi mümkün olduğunca geçici olarak kaydedin. Çünkü geçici hizmetleri tasarlamak basittir. Genelde çoklu iş parçacığı ve bellek sızıntıları umrunda değil ve hizmetin kısa bir ömrü olduğunu biliyorsunuz.
  • Kapsamlı servis ömrünü dikkatli kullanın, çünkü çocuk servis kapsamları oluşturursanız veya bu hizmetleri web dışı bir uygulamadan kullanırsanız zor olabilir.
  • Tekli kullanım ömrünü dikkatli kullanın, o zamandan beri çok iş parçacıklı ve olası bellek sızıntısı sorunlarıyla ilgilenmeniz gerekir.
  • Tektonlu bir servisten geçici veya kapsamlı bir servise bağlı olmayın. Çünkü geçici servis, bir singleton servis enjekte ettiğinde singleton örneği olur ve geçici servis böyle bir senaryoyu destekleyecek şekilde tasarlanmamışsa sorun yaratabilir. ASP.NET Core’un varsayılan DI kabı zaten böyle durumlarda istisnalar ortaya koyuyor.

Bir Yöntem Organında Servisleri Çözme

Bazı durumlarda, servisinizin bir yönteminde başka bir servisi çözmeniz gerekebilir. Bu gibi durumlarda, kullandıktan sonra servisi serbest bıraktığınızdan emin olun. Bunu sağlamanın en iyi yolu, hizmet kapsamı oluşturmaktır. Örnek:

kamu sınıfı PriceCalculator
{
    özel salt okunur IServiceProvider _serviceProvider;
    genel PriceCalculator (IServiceProvider hizmetiProvider)
    {
        _serviceProvider = serviceProvider;
    }
    halka açıklık yüzdesi Hesapla (Ürün ürünü, int sayısı,
      TaxStrategyServiceType yazın)
    {
        kullanma (var kapsam = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) kapsamı.
              .GetRequiredService (taxStrategyServiceType);
            var fiyat = ürün.Fiyat * sayısı;
            iade fiyatı + taxStrategy.CalculateTax (fiyat);
        }
    }
}

PriceCalculator, yapıcısına IServiceProvider'ı enjekte eder ve bir alana atar. PriceCalculator daha sonra bir alt hizmet kapsamı oluşturmak için Calculate yönteminin içinde kullanır. Enjekte edilen _serviceProvider örneği yerine, hizmetleri çözmek için kapsam.ServiceProvider kullanır. Böylece, kapsamdan çözülen tüm hizmetler, kullanım ifadesinin sonunda otomatik olarak serbest bırakılır / elden çıkarılır.

İyi Uygulamalar:

  • Bir hizmeti bir yöntem gövdesinde çözüyorsanız, çözülen hizmetlerin doğru şekilde yayınlandığından emin olmak için her zaman bir çocuk hizmeti kapsamı oluşturun.
  • Bir yöntem IServiceProvider'ı bir argüman olarak alırsa, bırakmadan / elden çıkarmaya aldırmadan doğrudan hizmetleri doğrudan çözebilirsiniz. Hizmet kapsamı oluşturmak / yönetmek, yönteminizi çağıran kodun sorumluluğundadır. Bu prensibi takip etmek kodunuzu daha temiz hale getirir.
  • Çözülmüş bir servise referans tutmayın! Aksi takdirde, bellek sızıntısına neden olabilir ve daha sonra nesne referansını kullandığınızda atılan bir servise erişebilirsiniz (çözülen servis singleton olmadığı sürece).

Singleton Hizmetler

Singleton hizmetleri genellikle bir başvuru durumunu korumak için tasarlanmıştır. Önbellek, uygulama durumlarına iyi bir örnektir. Örnek:

genel sınıf FileService
{
    özel salt okunur ConcurrentDictionary  _cache;
    genel FileService ()
    {
        _cache = new ConcurrentDictionary  ();
    }
    public byte [] GetFileContent (string filePath)
    {
        return _cache.GetOrAdd (filePath, _ =>
        {
            Döndür File.ReadAllBytes (filePath);
        });
    }
}

FileService, disk okumalarını azaltmak için sadece dosya içeriğini önbelleğe alır. Bu hizmet tekil olarak kaydedilmelidir. Aksi takdirde, önbellek beklendiği gibi çalışmayacak.

İyi Uygulamalar:

  • Hizmetin bir durumu varsa, o duruma güvenli bir şekilde erişmesi gerekir. Çünkü tüm istekler aynı anda hizmetin aynı örneğini kullanıyor. İş parçacığı güvenliğini sağlamak için sözlük yerine ConcurrentDictionary kullandım.
  • Singleton servislerinin kapsamlı veya geçici servislerini kullanmayın. Çünkü, geçici hizmetler iş parçacığı güvenli olacak şekilde tasarlanmamış olabilir. Bunları kullanmak zorundaysanız, bu hizmetleri kullanırken çoklu iş parçacığına dikkat edin (örneğin kilidi kullanın).
  • Bellek sızıntılarına genellikle tekil servisler neden olur. Başvurunun sonuna kadar serbest bırakılmaz / atılmazlar. Bu nedenle, sınıfları başlatırlar (veya enjekte ederlerse), ancak serbest bırakmazlar / elden çıkarmazlarsa, uygulamanın sonuna kadar bellekte kalırlar. Bunları doğru zamanda salıverdiğiniz / elden çıkardığınızdan emin olun. Yukarıdaki Yöntem Gövdesi Bölümündeki Hizmetleri Çözme bölümüne bakın.
  • Verileri önbelleğe alırsanız (bu örnekte dosya içeriği), orijinal veri kaynağı değiştiğinde (bu örnekte diskte önbelleğe alınmış bir dosya değiştiğinde) önbelleğe alınmış verileri güncellemek / geçersiz kılmak için bir mekanizma oluşturmalısınız.

Kapsamlı Hizmetler

Kapsamlı kullanım ömrü önce web isteği başına veri depolamak için iyi bir aday gibi görünüyor. Çünkü ASP.NET Core web talebi başına bir servis kapsamı yaratır. Bu nedenle, bir hizmeti kapsamlı olarak kaydederseniz, web isteği sırasında paylaşılabilir. Örnek:

genel sınıf RequestItemsService
{
    özel okumalar Sözlük  _items;
    Genel RequestItemsService ()
    {
        _items = new Dictionary  ();
    }
    public void Set (dize adı, nesne değeri)
    {
        _items [name] = değer;
    }
    public object Al (dize adı)
    {
        return _items [isim];
    }
}

RequestItemsService’i kapsamlı olarak kaydedip iki farklı hizmete enjekte ederseniz, aynı RequestItemsService örneğini paylaşacakları için başka bir hizmetten eklenmiş bir öğe alabilirsiniz. Kapsamlı hizmetlerden beklediğimiz budur.

Ama .. gerçek her zaman böyle olmayabilir. Bir alt hizmet kapsamı oluşturur ve RequestItemsService'i alt kapsamdan çözerseniz, RequestItemsService'in yeni bir örneğini alırsınız ve beklediğiniz gibi çalışmaz. Bu nedenle, kapsamlı hizmet her zaman web isteği başına örnek anlamına gelmez.

Böyle açık bir hata yapmadığınızı düşünebilirsiniz (bir çocuk kapsamı kapsamını düzelterek). Ancak, bu bir hata değildir (çok düzenli kullanım) ve durum bu kadar basit olmayabilir. Servisleriniz arasında büyük bir bağımlılık grafiği varsa, birinin bir çocuk kapsamı yaratıp yaratmadığını ve başka bir servisi enjekte eden bir hizmeti çözüp çözemediğini, nihayet bir kapsamda servis enjekte eden olduğunu bilemezsiniz.

İyi pratik:

  • Kapsamlı bir servis, bir web isteğinde çok fazla servis tarafından enjekte edildiği bir optimizasyon olarak düşünülebilir. Böylece, tüm bu servisler aynı web talebi sırasında servisin tek bir örneğini kullanacaktır.
  • Kapsamlı hizmetlerin iş parçacığı güvenliği için tasarlanmasına gerek yoktur. Çünkü normalde tek bir web isteği / iş parçacığı tarafından kullanılmaları gerekir. Ancak… bu durumda, servis kapsamlarını farklı dişler arasında paylaşmamalısınız!
  • Bir web isteğindeki diğer servisler arasında veri paylaşmak için kapsamlı bir servis tasarlıyorsanız dikkatli olun (yukarıda açıklanmıştır). Her web isteği verisini, bunu yapmanın daha güvenli bir yolu olan HttpContext'in (buna erişmek için IHttpContextAccessor enjekte edilir) içinde saklayabilirsiniz. HttpContext’in ömrü kapsam dışı. Aslında, DI'ye kayıtlı değildir (bu yüzden enjekte etmediniz, bunun yerine IHttpContextAccessor'u enjekte edin). HttpContextAccessor uygulaması, bir web isteği sırasında aynı HttpContext'i paylaşmak için AsyncLocal kullanır.

Sonuç

Bağımlılık enjeksiyonu ilk başta kullanımı basit görünmektedir, ancak bazı katı ilkelere uymazsanız potansiyel çoklu iş parçacığı ve bellek sızıntısı sorunları vardır. ASP.NET Boilerplate çerçevesinin geliştirilmesi sırasındaki deneyimlerime dayanarak bazı iyi ilkeleri paylaştım.