Suntikan Ketergantungan Teras ASP.NET Terutama Amalan, Tips & Trik Terbaik

Dalam artikel ini, saya akan berkongsi pengalaman dan cadangan saya mengenai penggunaan Suntikan Ketergantungan dalam aplikasi Inti ASP.NET. Motivasi di sebalik prinsip-prinsip ini adalah;

  • Merancang perkhidmatan dan kebergantungan mereka dengan berkesan.
  • Mencegah isu-isu berbilang skrip.
  • Mencegah kebocoran memori.
  • Mencegah pepijat yang berpotensi.

Artikel ini menganggap bahawa anda sudah terbiasa dengan Ketergantungan Suntikan dan Inti ASP.NET dalam tahap asas. Sekiranya tidak, sila baca dokumentasi Suntikan Ketergantungan Teras ASP.NET terlebih dahulu.

Asas-asas

Suntikan Pembina

Suntikan pembina digunakan untuk mengisytiharkan dan mendapatkan kebergantungan perkhidmatan pada pembinaan perkhidmatan. Contoh:

ProductService kelas awam
{
    private readonly IProductRepository _productRepository;
    ProductService awam (productProductRepositoryRepository)
    {
        _productRepository = productRepository;
    }
    kekosongan awam Padam (int id)
    {
        _productRepository.Delete (id);
    }
}

ProductService menyuntik IProductRepository sebagai kebergantungan dalam pembangunnya kemudian menggunakannya di dalam kaedah Padam.

Amalan Baik:

  • Tentukan kebergantungan yang diperlukan secara jelas dalam pembekal perkhidmatan. Oleh itu, perkhidmatan itu tidak boleh dibina tanpa kebergantungannya.
  • Berikan pergantungan yang disuntik ke medan / harta sahaja yang dibaca (untuk mengelakkan secara tidak sengaja memberikan nilai lain kepadanya di dalam suatu kaedah).

Suntikan Harta

Bekas suntikan ketergantungan standard ASP.NET Core tidak menyokong suntikan harta. Tetapi anda boleh menggunakan bekas lain yang menyokong suntikan harta benda. Contoh:

menggunakan Microsoft.Extensions.Logging;
menggunakan Microsoft.Extensions.Logging.Abstractions;
ruang nama MyApp
{
    ProductService kelas awam
    {
        awam ILogger  Logger {get; menetapkan; }
        private readonly IProductRepository _productRepository;
        ProductService awam (productProductRepositoryRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger  Instance;
        }
        kekosongan awam Padam (int id)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ "Membuang produk dengan id = {id}");
        }
    }
}

ProductService mengisytiharkan harta Logger dengan setter awam. Bekas suntikan ketahanan boleh menetapkan Logger jika tersedia (terdaftar ke bekas DI sebelum).

Amalan Baik:

  • Gunakan suntikan hartanah hanya untuk dependencies pilihan. Ini bermakna perkhidmatan anda boleh berfungsi dengan sempurna tanpa kebergantungan ini.
  • Gunakan Corak Objek Null (seperti dalam contoh ini) jika boleh. Jika tidak, sentiasa semak null semasa menggunakan pergantungan.

Pencari Perkhidmatan

Corak pencari perkhidmatan adalah cara lain untuk mendapatkan dependensi. Contoh:

ProductService kelas awam
{
    private readonly IProductRepository _productRepository;
    secara peribadi baca ILogger  _logger;
    ProductService awam (perkhidmatan IS ProviderProviderProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > () ??
            NullLogger  Instance;
    }
    kekosongan awam Padam (int id)
    {
        _productRepository.Delete (id);
        _logger.LogInformation ($ "Membuang produk dengan id = {id}");
    }
}

ProductService menyuntik IServiceProvider dan menyelesaikan dependensi menggunakannya. GetRequiredService melemparkan pengecualian jika kebergantungan yang diminta tidak didaftarkan sebelum ini. Sebaliknya, GetService hanya mengembalikan batal dalam kes itu.

Apabila anda menyelesaikan perkhidmatan di dalam pembina, mereka dibebaskan apabila perkhidmatan dikeluarkan. Oleh itu, anda tidak mengambil berat tentang melepaskan / membuang perkhidmatan yang diselesaikan di dalam pembina (sama seperti pembina dan suntikan harta benda).

Amalan Baik:

  • Jangan gunakan corak pencari perkhidmatan sekiranya mungkin (jika jenis perkhidmatan diketahui dalam masa pembangunan). Kerana ia menjadikan kebergantungan tersirat. Ini bermakna tidak mungkin untuk melihat dependensi dengan mudah semasa membuat contoh perkhidmatan. Ini amat penting untuk ujian unit di mana anda mungkin mahu mengejek beberapa kebergantungan perkhidmatan.
  • Selesaikan kebergantungan dalam pembina perkhidmatan jika mungkin. Menyelesaikan dalam kaedah perkhidmatan menjadikan aplikasi anda lebih rumit dan rawan ralat. Saya akan menutup masalah & penyelesaian di bahagian seterusnya.

Waktu Hidup Perkhidmatan

Terdapat tiga hayat perkhidmatan dalam Suntikan Ketergantungan Teras ASP.NET:

  1. Perkhidmatan sementara dibuat setiap kali mereka disuntik atau diminta.
  2. Perkhidmatan scoped dibuat setiap skop. Dalam aplikasi web, setiap permintaan web mewujudkan skop perkhidmatan yang berasingan. Itu bererti perkhidmatan sampingan secara umumnya dicipta setiap permintaan web.
  3. Perkhidmatan Singleton dicipta setiap bekas DI. Ini secara amnya bermakna bahawa mereka hanya dibuat satu kali setiap aplikasi dan kemudian digunakan untuk keseluruhan masa hayat aplikasi.

Bekas DI menjejaki semua perkhidmatan yang diselesaikan. Perkhidmatan dibebaskan dan dilupuskan apabila hayat mereka berakhir:

  • Sekiranya perkhidmatan tersebut mempunyai kebergantungan, ia juga dilancarkan secara automatik dan dilupuskan.
  • Jika perkhidmatan mengimplementasikan antara muka IDisposable, Kaedah buang secara automatik dipanggil pembebasan perkhidmatan.

Amalan Baik:

  • Daftar perkhidmatan anda sebagai sementara jika mungkin. Kerana ia mudah untuk mereka bentuk perkhidmatan sementara. Anda secara amnya tidak peduli tentang kebocoran multi-threading dan memori dan anda tahu perkhidmatan itu mempunyai kehidupan yang singkat.
  • Gunakan jangka hayat perkhidmatan yang berhati-hati kerana ia boleh menjadi rumit jika anda membuat skop perkhidmatan kanak-kanak atau menggunakan perkhidmatan ini dari aplikasi bukan web.
  • Gunakan sepanjang hayat tunggal dengan berhati-hati sejak itu, anda perlu berhadapan dengan masalah kebocoran memori yang multi-threading dan potensi.
  • Jangan bergantung pada perkhidmatan sementara atau scoped dari perkhidmatan tunggal. Kerana perkhidmatan sementara menjadi contoh singleton apabila perkhidmatan tunggal menyuntikkannya dan itu boleh menyebabkan masalah jika perkhidmatan sementara tidak dirancang untuk menyokong senario sedemikian. Kontena lalai ASP.NET Core telah melemparkan pengecualian dalam kes sedemikian.

Menyelesaikan Perkhidmatan dalam Badan Cara

Dalam sesetengah kes, anda mungkin perlu menyelesaikan perkhidmatan lain dalam kaedah perkhidmatan anda. Dalam kes sedemikian, pastikan anda melepaskan perkhidmatan selepas digunakan. Cara terbaik untuk memastikannya adalah untuk mewujudkan skop perkhidmatan. Contoh:

PriceCalculator kelas awam
{
    swasta readonly _serviceProvider IServiceProvider;
    PriceCalculator awam (perkhidmatan IS ProviderProviderProvider)
    {
        _serviceProvider = serviceProvider;
    }
    terapung awam Kira (Produk produk, bilangan int,
      Jenis cukaiStrategyServiceType)
    {
        menggunakan (var scope = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) scope.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            var harga = product.Price * count;
            harga pulangan + cukaiStrategy.CalculateTax (harga);
        }
    }
}

PriceCalculator menyuntik IServiceProvider dalam pembangunnya dan menyerahkannya ke lapangan. PriceCalculator kemudian menggunakannya di dalam kaedah Kira untuk membuat skop perkhidmatan kanak-kanak. Ia menggunakan scope.ServiceProvider untuk menyelesaikan perkhidmatan, bukan contoh _serviceProvider yang disuntik. Oleh itu, semua perkhidmatan yang diselesaikan dari skop secara automatik dikeluarkan / dilupuskan pada akhir pernyataan penggunaan.

Amalan Baik:

  • Sekiranya anda menyelesaikan perkhidmatan dalam badan kaedah, sentiasa buat skop perkhidmatan kanak-kanak untuk memastikan bahawa perkhidmatan yang telah diselesaikan dikeluarkan dengan betul.
  • Sekiranya satu kaedah mendapat IServiceProvider sebagai hujah, maka anda boleh menyelesaikan perkhidmatan secara langsung daripada itu tanpa peduli tentang melepaskan / melupuskan. Mewujudkan / menguruskan skop perkhidmatan adalah tanggungjawab kod yang memanggil kaedah anda. Mengikuti prinsip ini menjadikan pembersih kod anda.
  • Jangan memegang sebutan mengenai perkhidmatan yang diselesaikan! Jika tidak, ia boleh menyebabkan kebocoran memori dan anda akan mengakses perkhidmatan yang dilupuskan apabila anda menggunakan rujukan objek kemudian (melainkan perkhidmatan yang diselesaikan adalah tunggal).

Perkhidmatan Singleton

Perkhidmatan Singleton secara amnya direka untuk mengekalkan keadaan aplikasi. Cache adalah contoh yang baik dari aplikasi aplikasi. Contoh:

FileService kelas awam
{
    bacaan peribadi ConcurrentDictionary  _cache;
    FileService awam ()
    {
        _cache = ConcurrentDictionary baru  ();
    }
    bait awam [] GetFileContent (string filePath)
    {
        kembali _cache.GetOrAdd (filePath, _ =>
        {
            kembali File.ReadAllBytes (filePath);
        });
    }
}

FileService hanya cache kandungan fail untuk mengurangkan bacaan cakera. Perkhidmatan ini hendaklah didaftarkan sebagai tunggal. Jika tidak, caching tidak akan berfungsi seperti yang diharapkan.

Amalan Baik:

  • Jika perkhidmatan memegang keadaan, ia harus mengakses keadaan tersebut dengan cara yang selamat. Kerana semua permintaan serentak menggunakan contoh perkhidmatan yang sama. Saya menggunakan ConcurrentDictionary bukan Kamus untuk memastikan keselamatan thread.
  • Jangan gunakan perkhidmatan yang berskala atau fana dari perkhidmatan tunggal. Kerana, perkhidmatan sementara mungkin tidak dirancang untuk menjadi benang selamat. Sekiranya anda perlu menggunakannya, maka berhati-hati dengan pelbagai threading semasa menggunakan perkhidmatan ini (menggunakan kunci misalnya).
  • Kebocoran memori biasanya disebabkan oleh perkhidmatan tunggal. Mereka tidak dibebaskan / dilupuskan sehingga akhir permohonan. Jadi, jika mereka memberi contoh kelas (atau menyuntik) tetapi tidak membebaskan / melupuskan mereka, mereka juga akan kekal dalam ingatan sehingga akhir permohonan. Pastikan anda melepaskan / melupuskannya pada masa yang sesuai. Lihat Perkhidmatan Menyelesaikan di bahagian Badan Cara di atas.
  • Jika anda cache data (kandungan fail dalam contoh ini), anda perlu membuat mekanisme untuk mengemas kini / membatalkan data cache semasa sumber data asal berubah (apabila fail cache disimpan pada disk untuk contoh ini).

Perkhidmatan Scoped

Kehidupan seumur hidup pertama kelihatan sebagai calon yang baik untuk menyimpan setiap data permintaan web. Kerana ASP.NET Core mewujudkan skop perkhidmatan untuk setiap permintaan web. Oleh itu, jika anda mendaftarkan perkhidmatan sebagai scoped, ia boleh dikongsi semasa permintaan web. Contoh:

RequestItemsService kelas awam
{
    peribadi bacaan Kamus  _items;
    RequestItemsService umum ()
    {
        _items = Kamus baru  ();
    }
    kekosongan awam Tetapkan (nama rentetan, nilai objek)
    {
        _items [nama] = nilai;
    }
    Objek awam Dapatkan (nama rentetan)
    {
        kembali _items [nama];
    }
}

Jika anda mendaftarkan RequestItemsService sebagai scoped dan menyuntikkannya kepada dua perkhidmatan yang berbeza, maka anda boleh mendapatkan item yang ditambahkan dari perkhidmatan lain kerana mereka akan berkongsi contoh RequestItemsService yang sama. Itulah yang kami harapkan daripada perkhidmatan yang terkawal.

Tetapi .. hakikatnya tidak semestinya seperti itu. Jika anda membuat skop perkhidmatan kanak-kanak dan menyelesaikan RequestItemsService dari skop kanak-kanak, maka anda akan mendapat contoh baru dari RequestItemsService dan ia tidak akan berfungsi seperti yang anda harapkan. Oleh itu, perkhidmatan sampingan tidak selalunya bermaksud contoh per permintaan web.

Anda mungkin berfikir bahawa anda tidak membuat kesilapan yang jelas (menyelesaikan masalah di dalam skop kanak-kanak). Tetapi, ini bukan kesilapan (penggunaan yang sangat kerap) dan kes itu tidak semudah itu. Sekiranya terdapat graf pergantungan yang besar di antara perkhidmatan anda, anda tidak boleh mengetahui sama ada sesiapa yang mencipta skop kanak-kanak dan menyelesaikan perkhidmatan yang menyuntik perkhidmatan lain ... yang akhirnya menyuntik perkhidmatan sampingan.

Latihan yang baik:

  • Perkhidmatan scoped boleh dianggap sebagai pengoptimalan di mana ia disuntik oleh terlalu banyak perkhidmatan dalam permintaan web. Oleh itu, semua perkhidmatan ini akan menggunakan satu contoh perkhidmatan semasa permintaan web yang sama.
  • Perkhidmatan yang dilindungi tidak perlu direka sebagai benang selamat. Kerana, mereka harus biasanya digunakan oleh satu permintaan / thread web. Tetapi ... dalam kes itu, anda tidak sepatutnya berkongsi skop perkhidmatan antara benang yang berbeza!
  • Berhati-hati jika anda merancang perkhidmatan yang berskala untuk berkongsi data antara perkhidmatan lain dalam permintaan web (dijelaskan di atas). Anda boleh menyimpan setiap data permintaan web di dalam HttpContext (menyuntik IHttpContextAccessor untuk mengaksesnya) yang merupakan cara yang lebih selamat untuk melakukannya. Kehidupan HttpContext tidak dikawal. Sebenarnya, ia tidak didaftarkan kepada DI sama sekali (itulah sebabnya anda tidak menyuntiknya, tetapi menyuntik IHttpContextAccessor sebaliknya). Pelaksanaan HttpContextAccessor menggunakan AsyncLocal untuk berkongsi HttpContext yang sama semasa permintaan web.

Kesimpulannya

Suntikan ketergantungan seolah-olah mudah untuk digunakan pada mulanya, tetapi terdapat potensi multi-threading dan masalah kebocoran memori jika anda tidak mengikuti beberapa prinsip yang ketat. Saya berkongsi beberapa prinsip yang baik berdasarkan pengalaman saya sendiri semasa pembangunan rangka kerja Boilerplate ASP.NET.