ASP.NET Inti Ketergantungan Injeksi Praktik Terbaik, Kiat & Trik

Pada artikel ini, saya akan berbagi pengalaman dan saran saya tentang penggunaan Dependency Injection dalam aplikasi ASP.NET Core. Motivasi di balik prinsip-prinsip ini adalah;

  • Mendesain layanan dan ketergantungannya secara efektif.
  • Mencegah masalah multi-threading.
  • Mencegah kebocoran memori.
  • Mencegah bug potensial.

Artikel ini mengasumsikan bahwa Anda sudah terbiasa dengan Dependency Injection dan ASP.NET Core di tingkat dasar. Jika tidak, silakan baca dokumentasi ASP.NET Core Dependency Injection terlebih dahulu.

Dasar-dasar

Injeksi Konstruktor

Injeksi konstruktor digunakan untuk menyatakan dan mendapatkan dependensi suatu layanan pada konstruksi layanan. Contoh:

ProductService kelas publik
{
    private readonly IProductRepository _productRepository;
    Public ProductService (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    public void Delete (int id)
    {
        _productRepository.Delete (id);
    }
}

ProductService menyuntikkan IProductRepository sebagai dependensi dalam konstruktornya lalu menggunakannya di dalam metode Delete.

Latihan yang baik:

  • Tetapkan dependensi yang diperlukan secara eksplisit di konstruktor layanan. Dengan demikian, layanan tidak dapat dibangun tanpa ketergantungannya.
  • Tetapkan ketergantungan yang disuntikkan ke bidang / properti hanya baca (untuk mencegah penetapan nilai lain secara tidak sengaja di dalam metode).

Injeksi Properti

Wadah injeksi ketergantungan standar ASP.NET Core tidak mendukung injeksi properti. Tetapi Anda dapat menggunakan wadah lain yang mendukung injeksi properti. Contoh:

menggunakan Microsoft.Extensions.Logging;
menggunakan Microsoft.Extensions.Logging.Abstractions;
namespace MyApp
{
    ProductService kelas publik
    {
        ILogger publik  Logger {get; set; }
        private readonly IProductRepository _productRepository;
        Public ProductService (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger  .Instance;
        }
        public void Delete (int id)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ "Menghapus produk dengan id = {id}");
        }
    }
}

ProductService mendeklarasikan properti Logger dengan setter publik. Kontainer injeksi ketergantungan dapat mengatur Logger jika tersedia (terdaftar ke wadah DI sebelumnya).

Latihan yang baik:

  • Gunakan injeksi properti hanya untuk dependensi opsional. Itu berarti layanan Anda dapat berfungsi dengan baik tanpa disediakan dependensi ini.
  • Gunakan Null Object Pattern (seperti dalam contoh ini) jika memungkinkan. Jika tidak, selalu periksa nol saat menggunakan dependensi.

Pencari Lokasi Layanan

Pola pencari lokasi adalah cara lain untuk mendapatkan dependensi. Contoh:

ProductService kelas publik
{
    private readonly IProductRepository _productRepository;
    ILogger readonly pribadi  _logger;
    ProductService publik (IServiceProvider serviceProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .Dapatkan Layanan > () ??
            NullLogger  .Instance;
    }
    public void Delete (int id)
    {
        _productRepository.Delete (id);
        _logger.LogInformation ($ "Menghapus produk dengan id = {id}");
    }
}

ProductService menyuntikkan IServiceProvider dan menyelesaikan dependensi menggunakannya. GetRequiredService melempar pengecualian jika ketergantungan yang diminta tidak terdaftar sebelumnya. Di sisi lain, GetService hanya mengembalikan null dalam kasus itu.

Ketika Anda menyelesaikan layanan di dalam konstruktor, mereka dirilis saat layanan dirilis. Jadi, Anda tidak peduli tentang melepaskan / membuang layanan yang diselesaikan di dalam konstruktor (seperti halnya konstruktor dan injeksi properti).

Latihan yang baik:

  • Jangan menggunakan pola pencari lokasi layanan sedapat mungkin (jika jenis layanan diketahui dalam waktu pengembangan). Karena itu membuat dependensi tersirat. Itu berarti tidak mungkin untuk melihat dependensi dengan mudah saat membuat instance dari layanan. Ini sangat penting untuk pengujian unit di mana Anda mungkin ingin mengejek beberapa dependensi layanan.
  • Atasi dependensi dalam konstruktor layanan jika memungkinkan. Mengatasi masalah dalam metode layanan membuat aplikasi Anda lebih rumit dan rentan kesalahan. Saya akan membahas masalah & solusi di bagian selanjutnya.

Waktu Hidup Layanan

Ada tiga masa hidup layanan di ASP.NET Core Dependency Injection:

  1. Layanan sementara dibuat setiap kali mereka disuntikkan atau diminta.
  2. Layanan tercakup dibuat per cakupan. Dalam aplikasi web, setiap permintaan web menciptakan ruang lingkup layanan baru yang terpisah. Itu berarti layanan mencakup umumnya dibuat per permintaan web.
  3. Layanan singleton dibuat per kontainer DI. Itu umumnya berarti bahwa mereka dibuat hanya satu kali per aplikasi dan kemudian digunakan untuk seumur hidup aplikasi.

Kontainer DI melacak semua layanan yang diselesaikan. Layanan dirilis dan dibuang ketika masa hidupnya berakhir:

  • Jika layanan memiliki dependensi, mereka juga secara otomatis dirilis dan dibuang.
  • Jika layanan mengimplementasikan antarmuka IDisposable, metode Buang secara otomatis dipanggil pada rilis layanan.

Latihan yang baik:

  • Daftarkan layanan Anda sesaat mungkin. Karena sederhana untuk merancang layanan sementara. Anda biasanya tidak peduli dengan kebocoran multi-threading dan memori dan Anda tahu layanan ini singkat.
  • Gunakan layanan scoped seumur hidup dengan hati-hati karena ini bisa rumit jika Anda membuat cakupan layanan anak atau menggunakan layanan ini dari aplikasi non-web.
  • Gunakan singleton seumur hidup dengan hati-hati sejak itu Anda harus berurusan dengan multi-threading dan potensi masalah kebocoran memori.
  • Jangan bergantung pada layanan sementara atau cakupan dari layanan tunggal. Karena layanan sementara menjadi contoh tunggal ketika layanan tunggal menyuntikkannya dan itu dapat menyebabkan masalah jika layanan sementara tidak dirancang untuk mendukung skenario seperti itu. Wadah DI standar ASP.NET Core sudah melempar pengecualian dalam kasus tersebut.

Menyelesaikan Layanan di Badan Metode

Dalam beberapa kasus, Anda mungkin perlu menyelesaikan layanan lain dengan metode layanan Anda. Dalam kasus seperti itu, pastikan Anda merilis layanan setelah penggunaan. Cara terbaik untuk memastikan itu adalah menciptakan ruang lingkup layanan. Contoh:

PriceCalculator kelas publik
{
    IServiceProvider readonly pribadi _serviceProvider;
    public PriceCalculator (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    public float Calculate (Produk produk, jumlah int,
      Ketik taxStrategyServiceType)
    {
        menggunakan (var scope = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) scope.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            harga var = produk. Harga * dihitung;
            harga pengembalian + taxStrategy.CalculateTax (harga);
        }
    }
}

PriceCalculator menyuntikkan IServiceProvider dalam konstruktornya dan menetapkannya ke bidang. PriceCalculator kemudian menggunakannya di dalam metode Calculate untuk membuat ruang lingkup layanan anak. Ia menggunakan scope.ServiceProvider untuk menyelesaikan layanan, alih-alih instance _serviceProvider yang disuntikkan. Dengan demikian, semua layanan yang diselesaikan dari ruang lingkup secara otomatis dirilis / dibuang pada akhir pernyataan menggunakan.

Latihan yang baik:

  • Jika Anda menyelesaikan layanan di badan metode, selalu buat ruang lingkup layanan anak untuk memastikan bahwa layanan yang diselesaikan dirilis dengan benar.
  • Jika suatu metode mendapatkan IServiceProvider sebagai argumen, maka Anda dapat langsung menyelesaikan layanan darinya tanpa peduli melepaskan / membuang. Membuat / mengelola ruang lingkup layanan adalah tanggung jawab kode yang memanggil metode Anda. Mengikuti prinsip ini membuat kode Anda lebih bersih.
  • Jangan memegang referensi ke layanan yang diselesaikan! Jika tidak, ini dapat menyebabkan kebocoran memori dan Anda akan mengakses ke layanan yang dibuang ketika Anda menggunakan referensi objek nanti (kecuali layanan yang diselesaikan adalah tunggal).

Layanan Singleton

Layanan Singleton umumnya dirancang untuk menjaga status aplikasi. Cache adalah contoh yang bagus dari status aplikasi. Contoh:

FileService kelas publik
{
    private readonly ConcurrentDictionary  _cache;
    Layanan File publik ()
    {
        _cache = ConcurrentDictionary baru  ();
    }
    byte publik [] GetFileContent (string filePath)
    {
        return _cache.GetOrAdd (filePath, _ =>
        {
            kembalikan File.ReadAllBytes (filePath);
        });
    }
}

FileService hanya menyimpan konten file untuk mengurangi pembacaan disk. Layanan ini harus terdaftar sebagai singleton. Jika tidak, caching tidak akan berfungsi seperti yang diharapkan.

Latihan yang baik:

  • Jika layanan memiliki status, itu harus mengakses ke status itu dengan cara yang aman. Karena semua permintaan secara bersamaan menggunakan contoh layanan yang sama. Saya menggunakan ConcurrentDictionary daripada Kamus untuk memastikan keamanan utas.
  • Jangan menggunakan layanan scoped atau sementara dari layanan singleton. Karena, layanan sementara mungkin tidak dirancang untuk aman utas. Jika Anda harus menggunakannya, maka rawat multi-threading saat menggunakan layanan ini (gunakan kunci misalnya).
  • Kebocoran memori umumnya disebabkan oleh layanan tunggal. Mereka tidak dirilis / dibuang sampai akhir aplikasi. Jadi, jika mereka membuat instance kelas (atau menyuntikkan) tetapi tidak melepaskan / membuangnya, mereka juga akan tetap dalam memori sampai akhir aplikasi. Pastikan Anda melepaskan / membuangnya pada waktu yang tepat. Lihat bagian Menyelesaikan Layanan di dalam Badan Metode di atas.
  • Jika Anda men-cache data (isi file dalam contoh ini), Anda harus membuat mekanisme untuk memperbarui / membatalkan data yang di-cache ketika sumber data asli berubah (ketika file yang di-cache berubah pada disk untuk contoh ini).

Layanan Lingkup

Scoped seumur hidup pertama tampaknya merupakan kandidat yang baik untuk menyimpan per data permintaan web. Karena ASP.NET Core menciptakan cakupan layanan per permintaan web. Jadi, jika Anda mendaftarkan layanan sebagai cakupan, itu dapat dibagikan saat permintaan web. Contoh:

RequestItemsService kelas publik
{
    private readonly Dictionary  _items;
    publik RequestItemsService ()
    {
        _items = Kamus baru  ();
    }
    public void Set (nama string, nilai objek)
    {
        _items [nama] = nilai;
    }
    objek publik Dapatkan (nama string)
    {
        return _items [name];
    }
}

Jika Anda mendaftarkan RequestItemsService sebagai ruang lingkup dan menyuntikkannya ke dua layanan yang berbeda, maka Anda bisa mendapatkan item yang ditambahkan dari layanan lain karena mereka akan berbagi contoh RequestItemsService yang sama. Itulah yang kami harapkan dari layanan cakupan.

Tapi .. faktanya mungkin tidak selalu seperti itu. Jika Anda membuat lingkup layanan anak dan menyelesaikan RequestItemsService dari lingkup anak, maka Anda akan mendapatkan contoh baru dari RequestItemsService dan itu tidak akan berfungsi seperti yang Anda harapkan. Jadi, layanan mencakup tidak selalu berarti contoh per permintaan web.

Anda mungkin berpikir bahwa Anda tidak membuat kesalahan yang jelas (menyelesaikan lingkup dalam lingkup anak). Tapi, ini bukan kesalahan (penggunaan yang sangat teratur) dan kasusnya mungkin tidak sesederhana itu. Jika ada grafik ketergantungan besar antara layanan Anda, Anda tidak bisa tahu apakah ada orang yang membuat ruang lingkup anak dan menyelesaikan layanan yang menyuntikkan layanan lain ... yang akhirnya menyuntikkan layanan cakupan.

Praktik yang Baik:

  • Layanan lingkup dapat dianggap sebagai optimasi di mana ia disuntikkan oleh terlalu banyak layanan dalam permintaan web. Dengan demikian, semua layanan ini akan menggunakan satu contoh layanan selama permintaan web yang sama.
  • Layanan lingkup tidak perlu dirancang sebagai aman-utas. Karena, mereka seharusnya digunakan oleh satu permintaan web / utas. Tapi ... dalam hal ini, Anda tidak boleh berbagi cakupan layanan antara utas berbeda!
  • Hati-hati jika Anda merancang layanan cakupan untuk berbagi data antara layanan lain dalam permintaan web (dijelaskan di atas). Anda dapat menyimpan per data permintaan web di dalam HttpContext (menyuntikkan IHttpContextAccessor untuk mengaksesnya) yang merupakan cara yang lebih aman untuk melakukan itu. Masa pakai HttpContext tidak dicakup. Sebenarnya, itu sama sekali tidak terdaftar di DI (itulah sebabnya Anda tidak menyuntikkannya, tetapi sebaliknya menyuntikkan IHttpContextAccessor sebagai gantinya). Implementasi HttpContextAccessor menggunakan AsyncLocal untuk berbagi HttpContext yang sama selama permintaan web.

Kesimpulan

Injeksi ketergantungan tampaknya mudah digunakan pada awalnya, tetapi ada potensi multi-threading dan masalah kebocoran memori jika Anda tidak mengikuti beberapa prinsip ketat. Saya berbagi beberapa prinsip yang baik berdasarkan pengalaman saya sendiri selama pengembangan kerangka ASP.NET Boilerplate.