Bagikan melalui


Panduan Injeksi Ketergantungan

Artikel ini menyediakan panduan umum dan praktik terbaik untuk menerapkan injeksi dependensi (DI) dalam aplikasi .NET.

Layanan desain untuk penyisipan ketergantungan

Saat merancang layanan untuk injeksi dependensi:

  • Hindari penggunaan kelas dan anggota statis yang bersifat stateful. Hindari membuat status global dengan merancang aplikasi untuk menggunakan layanan singleton sebagai gantinya.
  • Hindari instansiasi langsung kelas dependen dalam layanan. Instansiasi langsung mengaitkan kode dengan implementasi tertentu.
  • Membuat layanan kecil, diperhitungkan dengan baik, dan mudah diuji.

Jika kelas memiliki banyak dependensi yang disuntikkan, mungkin merupakan tanda bahwa kelas memiliki terlalu banyak tanggung jawab dan melanggar Prinsip Tanggung Jawab Tunggal (SRP). Usahakan untuk menata ulang kelas dengan memindahkan beberapa tanggung jawabnya ke kelas baru.

Pembuangan layanan

Kontainer bertanggung jawab untuk membersihkan jenis yang dibuatnya, dan memanggil Dispose pada IDisposable instans. Layanan yang diperoleh dari kontainer tidak boleh dimusnahkan oleh pengembang. Jika jenis atau pabrik terdaftar sebagai singleton, kontainer akan membuang singleton secara otomatis.

Dalam contoh berikut, layanan dibuat oleh kontainer layanan dan dibuang secara otomatis:

namespace ConsoleDisposable.Example;

public sealed class TransientDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(TransientDisposable)}.Dispose()");
}

Sekali pakai sebelumnya dimaksudkan untuk memiliki masa pakai sementara.

namespace ConsoleDisposable.Example;

public sealed class ScopedDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(ScopedDisposable)}.Dispose()");
}

Barang sekali pakai yang disebutkan sebelumnya dimaksudkan untuk memiliki masa pakai terbatas.

namespace ConsoleDisposable.Example;

public sealed class SingletonDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(SingletonDisposable)}.Dispose()");
}

Disposable sebelumnya dimaksudkan untuk memiliki masa pakai tunggal.

using ConsoleDisposable.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped<ScopedDisposable>();
builder.Services.AddSingleton<SingletonDisposable>();

using IHost host = builder.Build();

ExemplifyDisposableScoping(host.Services, "Scope 1");
Console.WriteLine();

ExemplifyDisposableScoping(host.Services, "Scope 2");
Console.WriteLine();

await host.RunAsync();

static void ExemplifyDisposableScoping(IServiceProvider services, string scope)
{
    Console.WriteLine($"{scope}...");

    using IServiceScope serviceScope = services.CreateScope();
    IServiceProvider provider = serviceScope.ServiceProvider;

    _ = provider.GetRequiredService<TransientDisposable>();
    _ = provider.GetRequiredService<ScopedDisposable>();
    _ = provider.GetRequiredService<SingletonDisposable>();
}

Konsol debug menunjukkan contoh output berikut setelah berjalan:

Scope 1...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

Scope 2...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

info: Microsoft.Hosting.Lifetime[0]
      Application started.Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
     Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
     Content root path: .\configuration\console-di-disposable\bin\Debug\net5.0
info: Microsoft.Hosting.Lifetime[0]
     Application is shutting down...
SingletonDisposable.Dispose()

Layanan tidak dibuat oleh wadah layanan

Pertimbangkan kode berikut:

// Register example service in IServiceCollection.
builder.Services.AddSingleton(new ExampleService());

Dalam kode sebelumnya:

  • ExampleService Instans tidak dibuat oleh kontainer layanan.
  • Kerangka kerja tidak membuang layanan secara otomatis.
  • Pengembang bertanggung jawab untuk menonaktifkan layanan.

Panduan IDisposable untuk instans sementara dan bersama

Sementara, masa pakai terbatas

Skenario

Aplikasi ini memerlukan instans IDisposable dengan masa pakai sementara untuk salah satu skenario berikut:

  • Instans dipecahkan dalam lingkup akar (kontainer akar).
  • Instance harus dibuang sebelum cakupan berakhir.

Solution

Gunakan pola factory untuk membuat instance di luar cakupan induk. Dalam situasi ini, aplikasi umumnya memiliki metode Create yang memanggil konstruktor tipe final secara langsung. Jika jenis akhir memiliki dependensi lain, pabrik dapat:

Instans bersama, masa pakai terbatas

Skenario

Aplikasi ini memerlukan instans bersama IDisposable di beberapa layanan, tetapi IDisposable instans harus memiliki masa pakai yang terbatas.

Solution

Daftarkan instans dengan masa pakai terlingkup. Gunakan IServiceScopeFactory.CreateScope untuk membuat baru IServiceScope. Gunakan IServiceProvider untuk mendapatkan layanan yang diperlukan. Hapus ruang lingkup saat tidak lagi diperlukan.

Pedoman umum IDisposable

  • Jangan mendaftarkan IDisposable instans dengan masa pakai sementara. Gunakan pola pabrik sebagai gantinya sehingga layanan yang dipecahkan dapat dibuang secara manual saat tidak lagi digunakan.
  • Jangan menentukan instans IDisposable dengan masa hidup sementara atau cakupan pada cakupan akar. Satu-satunya pengecualian untuk ini adalah jika aplikasi membuat atau membuat ulang dan membuang IServiceProvider, tetapi ini bukan pola yang ideal.
  • IDisposable Menerima dependensi melalui DI tidak mengharuskan penerima mengimplementasikan IDisposable dirinya sendiri. Penerima IDisposable dependensi tersebut tidak boleh memanggil Dispose.
  • Gunakan cakupan untuk mengontrol masa pakai layanan. Cakupan tidak hierarkis, dan tidak ada koneksi khusus di antara cakupan.

Untuk informasi selengkapnya tentang pembersihan sumber daya, lihat Menerapkan Dispose metode atau Menerapkan DisposeAsync metode. Selain itu, pertimbangkan layanan sementara sekilas yang ditangani oleh skenario kontainer karena berkaitan dengan pembersihan sumber daya.

Penggantian wadah layanan standar

Kontainer layanan bawaan dirancang untuk melayani kebutuhan kerangka kerja dan sebagian besar aplikasi konsumen. Sebaiknya gunakan kontainer bawaan kecuali Anda memerlukan fitur tertentu yang tidak didukungnya, seperti:

  • Penginjeksian properti
  • Kontainer anak
  • Manajemen masa pakai kustom
  • Func<T> dukungan untuk inisialisasi tunda
  • Pendaftaran berbasis konvensi

Kontainer pihak ketiga berikut dapat digunakan dengan aplikasi ASP.NET Core:

Keamanan Benang

Buat layanan singleton yang aman thread. Jika layanan singleton bergantung pada layanan transient, layanan transient mungkin juga memerlukan keamanan thread tergantung pada bagaimana layanan tersebut digunakan oleh singleton. Metode pabrik layanan singleton, seperti argumen kedua ke AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), tidak perlu aman utas. Seperti konstruktor tipe (static), dapat dipastikan hanya akan dipanggil sekali oleh satu utas.

Selain itu, proses penyelesaian layanan dari kontainer injeksi dependensi .NET bawaan aman. Setelah IServiceProvider atau IServiceScope dibangun, aman untuk menyelesaikan layanan secara bersamaan dari beberapa utas.

Nota

Keamanan thread container DI itu sendiri hanya menjamin bahwa membangun dan memecahkan layanan aman. Ini tidak membuat instans layanan yang diselesaikan sendiri aman utas. Setiap layanan (terutama singleton) yang memegang status mutable bersama harus menerapkan logika sinkronisasi sendiri jika diakses secara bersamaan.

Recommendations

  • async/await dan Task resolusi berbasis layanan tidak didukung. Karena C# tidak mendukung konstruktor asinkron, gunakan metode asinkron setelah menyelesaikan layanan secara sinkron.
  • Hindari menyimpan data dan konfigurasi langsung dalam kontainer layanan. Misalnya, keranjang belanja pengguna seharusnya tidak ditambahkan ke wadah layanan. Konfigurasi harus menggunakan pola opsi. Demikian pula, hindari objek "pemegang data" yang hanya ada untuk memungkinkan akses ke objek lain. Lebih baik meminta item aktual melalui DI.
  • Hindari akses statis ke layanan. Misalnya, hindari menangkap IApplicationBuilder.ApplicationServices sebagai bidang statis atau properti untuk digunakan di tempat lain.
  • Menjaga pabrik-pabrik DI tetap cepat dan sinkron.
  • Hindari menggunakan pola pencari lokasi layanan. Misalnya, jangan panggil GetService untuk mendapatkan instans layanan saat Anda dapat menggunakan DI sebagai gantinya.
  • Variasi pencari layanan lain yang harus dihindari adalah menyuntikkan pabrik yang menyelesaikan dependensi saat runtime. Kedua praktik ini mencampur strategi Inversi Kontrol .
  • Hindari panggilan ke BuildServiceProvider saat mengonfigurasi layanan. Biasanya, pemanggilan BuildServiceProvider terjadi ketika pengembang ingin menyelesaikan layanan saat mendaftarkan layanan lain. Sebagai gantinya, gunakan overload yang mencakup IServiceProvider untuk alasan ini.
  • Layanan sementara sekali pakai ditangkap oleh kontainer untuk dibuang. Ini dapat berubah menjadi kebocoran memori jika ditangani dari kontainer tingkat atas.
  • Aktifkan validasi cakupan untuk memastikan aplikasi tidak memiliki singleton yang menangkap layanan terlingkup. Untuk informasi selengkapnya, lihat Validasi Cakupan.
  • Hanya gunakan masa pakai singleton untuk layanan dengan status mereka sendiri yang mahal untuk dibuat atau dibagikan secara global. Hindari menggunakan masa pakai singleton untuk layanan yang tidak memiliki status sendiri. Sebagian besar kontainer .NET IoC menggunakan "Sementara" sebagai cakupan default. Pertimbangan dan kelemahan singleton:
    • Keamanan utas: Singleton harus diimplementasikan dengan cara yang aman untuk utas.
    • Coupling: Ini dapat menggabungkan permintaan yang tidak terkait.
    • Tantangan pengujian: Status bersama dan kopling dapat membuat pengujian unit lebih sulit.
    • Dampak memori: Singleton dapat menjaga grafik objek besar tetap hidup dalam memori selama masa pakai aplikasi.
    • Toleransi kesalahan: Jika singleton atau bagian mana pun dari pohon dependensinya gagal, sistem tidak dapat mudah dipulihkan.
    • Pemuatan ulang konfigurasi: Singleton umumnya tidak dapat mendukung "hot reload" nilai konfigurasi.
    • Kebocoran Cakupan: Sebuah singleton dapat secara tidak sengaja menangkap dependensi yang berjangka atau sementara, yang secara efektif mengubahnya menjadi singleton dan menyebabkan efek samping yang tidak diinginkan.
    • Overhead inisialisasi: Saat memproses layanan, kontainer IoC perlu menemukan instans singleton. Jika belum ada, perlu membuatnya dengan cara yang aman untuk utas. Sebaliknya, layanan sementara yang sifatnya sementara sangat murah untuk dibangun dan dihapus.

Seperti semua set rekomendasi, Anda mungkin mengalami situasi di mana mengabaikan rekomendasi diperlukan. Pengecualian jarang terjadi, dan sebagian besar merupakan kasus khusus dalam kerangka kerja itu sendiri.

DI adalah alternatif untuk pola akses objek statis/global. Anda mungkin tidak menyadari manfaat DI jika Anda mencampurnya dengan akses objek statis.

Contoh pola bermasalah

Selain pedoman dalam artikel ini, ada beberapa anti-pola yang harus Anda hindari. Beberapa pola yang tidak efektif ini adalah pembelajaran dari pengembangan runtime itu sendiri.

Peringatan

Ini adalah contoh anti-pola. Jangan salin kode, jangan gunakan pola-pola ini, dan hindari pola-pola ini dengan segala cara.

Layanan sementara sekali pakai yang diambil oleh kontainer

Ketika Anda mendaftarkan layanan sementara yang mengimplementasikan IDisposable, secara default kontainer DI memegang referensi ini. Tidak akan dibuang sampai wadahnya dibuang ketika aplikasi berhenti jika mereka diselesaikan dari wadah, atau sampai ruang lingkupnya dibuang jika mereka diselesaikan dari ruang lingkup. Kebocoran memori dapat terjadi jika diatasi dari tingkat kontainer.

Anti-pola: Sementara sekali pakai tanpa membuang. Jangan salin!

Dalam anti-pola sebelumnya, 1.000 ExampleDisposable objek diinisialisasi dan dijadikan akar. Instans tidak akan dibuang sampai serviceProvider tersebut dibuang.

Untuk informasi selengkapnya tentang men-debug kebocoran memori, lihat Men-debug kebocoran memori di .NET.

Pabrik Asinkron DI dapat menyebabkan kebuntuan

Istilah "DI factories" mengacu pada metode overload yang ada saat memanggil Add{LIFETIME}. Ada overload yang menerima Func<IServiceProvider, T> di mana T adalah layanan yang didaftarkan, dan parameter diberi nama implementationFactory. implementationFactory dapat disediakan sebagai ekspresi lambda, fungsi lokal, atau metode. Jika fabrik bersifat asinkron, dan Anda menggunakan Task<TResult>.Result, itu akan menyebabkan kebuntuan.

Anti-pola desain: Kebuntuan karena pabrik asinkron. Jangan menyalin!

Dalam kode sebelumnya, implementationFactory diberikan ekspresi lambda di mana badan memanggil Task<TResult>.Result pada metode yang mengembalikan Task<Bar>. Ini menyebabkan kebuntuan. Metode ini GetBarAsync hanya meniru operasi kerja asinkron dengan Task.Delay, dan kemudian memanggil GetRequiredService<T>(IServiceProvider).

Anti-pattern: Kebuntuan dengan masalah internal pada pabrik asinkron. Jangan salin!

Untuk informasi selengkapnya tentang panduan asinkron, lihat Pemrograman asinkron: Info dan saran penting. Untuk informasi selengkapnya men-debug kebuntuan, lihat Men-debug kebuntuan di .NET.

Saat Anda menjalankan pola antipola ini dan kebuntuan terjadi, Anda dapat melihat dua utas yang menunggu melalui jendela Tumpukan Paralel di Visual Studio. Untuk informasi selengkapnya, lihat Utas dan tugas di jendela Tumpukan Paralel.

Dependensi tawanan

Istilah "dependensi tawanan", yang dikoinkan oleh Mark Seemann, mengacu pada kesalahan konfigurasi masa pakai layanan, di mana layanan yang berumur lebih lama menyimpan tawanan layanan yang berumur lebih pendek.

Anti-pola: Ketergantungan terkurung. Jangan salin!

Dalam kode sebelumnya, Foo terdaftar sebagai singleton dan Bar terlingkup - yang di permukaan tampaknya valid. Namun, pertimbangkan implementasi Foo.

namespace DependencyInjection.AntiPatterns;

public class Foo(Bar bar)
{
}

Objek Foo memerlukan objek Bar, dan karena Foo adalah singleton, dan Bar berlingkup, ini adalah kesalahan konfigurasi. Saat ini, Foo hanya dibuat sekali, dan memegang Bar seumur hidupnya, yang lebih panjang dari masa hidup yang diinginkan sesuai cakupannya, Bar. Pertimbangkan untuk memvalidasi cakupan dengan meneruskan validateScopes: true ke BuildServiceProvider(IServiceCollection, Boolean). Saat Anda memvalidasi cakupan, Anda mendapatkan InvalidOperationException dengan pesan yang mirip dengan "Tidak dapat menggunakan layanan terlingkup 'Bar' dari singleton 'Foo'.".

Untuk informasi selengkapnya, lihat Validasi Cakupan.

Layanan dengan ruang lingkup sebagai singleton

Saat menggunakan layanan tercakup, jika Anda tidak membuat cakupan atau dalam cakupan yang ada, layanan menjadi singleton.

Anti-pola: Layanan terlingkup menjadi singleton. Jangan salin!

Dalam kode sebelumnya, Bar diperoleh dalam IServiceScope, itu benar. Anti-pola adalah pengambilan Bar yang berada di luar cakupan, dan variabel dinamai avoid untuk memperlihatkan contoh pengambilan yang tidak benar.

Lihat juga