Bagikan melalui


Panduan injeksi dependensi

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

Layanan desain untuk injeksi dependensi

Saat merancang layanan untuk injeksi dependensi:

  • Hindari kelas dan anggota statis yang stateful. Hindari membuat status global dengan merancang aplikasi untuk menggunakan layanan singleton sebagai gantinya.
  • Hindari instansiasi langsung kelas dependen dalam layanan. Instansiasi langsung menggabungkan kode ke 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). Coba refaktor kelas dengan memindahkan beberapa tanggung jawabnya ke kelas baru.

Pembuangan layanan

Kontainer bertanggung jawab untuk membersihkan jenis yang dibuatnya, dan panggilan Dispose pada IDisposable instans. Layanan yang diselesaikan dari kontainer tidak boleh dibuang 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()");
}

Sekali pakai sebelumnya dimaksudkan untuk memiliki masa pakai tercakup.

namespace ConsoleDisposable.Example;

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

Sekali pakai sebelumnya dimaksudkan untuk memiliki masa pakai singleton.

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 kontainer layanan

Pertimbangkan gambar 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 membuang 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 diselesaikan dalam cakupan akar (kontainer akar).
  • Instans harus dibuang sebelum cakupan berakhir.

Solusi

Gunakan pola pabrik untuk membuat instans di luar cakupan induk. Dalam situasi ini, aplikasi umumnya akan memiliki Create metode yang memanggil konstruktor jenis akhir 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.

Solusi

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

Pedoman umum IDisposable

  • Jangan mendaftarkan IDisposable instans dengan masa pakai sementara. Gunakan pola pabrik sebagai gantinya.
  • Jangan atasi IDisposable instans dengan masa pakai sementara atau cakupan dalam cakupan akar. Satu-satunya pengecualian untuk ini adalah jika aplikasi membuat/membuat ulang dan membuang IServiceProvider, tetapi ini bukan pola yang ideal.
  • IDisposable Menerima dependensi melalui DI tidak mengharuskan penerima mengimplementasikan IDisposable dirinya sendiri. Penerima dependensi tidak boleh memanggil Dispose dependensi tersebutIDisposable.
  • 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 Sekali Pakai yang diambil oleh skenario kontainer karena berkaitan dengan pembersihan sumber daya.

Penggantian kontainer layanan default

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:

  • Injeksi properti
  • Injeksi berdasarkan nama (.NET 7 dan versi yang lebih lama saja. Untuk informasi selengkapnya, lihat Layanan utama.)
  • Kontainer turunan
  • Manajemen masa pakai kustom
  • Func<T> dukungan untuk inisialisasi malas
  • Pendaftaran berbasis konvensi

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

Keamanan utas

Buat layanan singleton aman utas. Jika layanan singleton memiliki dependensi pada layanan sementara, layanan sementara mungkin juga memerlukan keamanan utas 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 jenis (static), dijamin hanya dipanggil sekali oleh satu utas.

Rekomendasi

  • async/await dan Task resolusi layanan berbasis 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, kelir belanja pengguna biasanya tidak boleh ditambahkan ke kontainer 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 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 pada waktu proses. Kedua praktik ini mencampur strategi Inversi Kontrol .
  • Hindari panggilan ke BuildServiceProvider saat mengonfigurasi layanan. BuildServiceProvider Panggilan biasanya terjadi ketika pengembang ingin menyelesaikan layanan saat mendaftarkan layanan lain. Sebagai gantinya IServiceProvider , gunakan kelebihan beban yang mencakup karena alasan ini.
  • Layanan sementara sekali pakai ditangkap oleh kontainer untuk dibuang. Ini dapat berubah menjadi kebocoran memori jika diselesaikan dari kontainer tingkat atas.
  • Aktifkan validasi cakupan untuk memastikan aplikasi tidak memiliki singleton yang menangkap layanan terlingkup. Untuk informasi selengkapnya, lihat Validasi cakupan.

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

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

Contoh anti-pola

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

Peringatan

Ini adalah contoh anti-pola, tidak menyalin kode, tidak menggunakan pola-pola ini, dan menghindari pola-pola ini dengan semua biaya.

Layanan sementara sekali pakai yang diambil oleh kontainer

Ketika Anda mendaftarkan layanan Sementara yang mengimplementasikan IDisposable, secara default kontainer DI akan berpegang pada referensi ini, dan bukan Dispose() sampai kontainer dibuang ketika aplikasi berhenti jika diselesaikan dari kontainer, atau sampai cakupan dibuang jika diselesaikan dari cakupan. Ini dapat berubah menjadi kebocoran memori jika diselesaikan dari tingkat kontainer.

Anti-pattern: Transient disposables without dispose. Do not copy!

Dalam anti-pola sebelumnya, 1.000 ExampleDisposable objek dibuat dan diakar. Mereka tidak akan dibuang sampai serviceProvider instans dibuang.

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

Pabrik Asinkron DI dapat menyebabkan kebuntuan

Istilah "PABRIK DI" mengacu pada metode kelebihan beban yang ada saat memanggil Add{LIFETIME}. Ada kelebihan beban yang Func<IServiceProvider, T> menerima tempat T layanan didaftarkan, dan parameter diberi nama implementationFactory. implementationFactory dapat disediakan sebagai ekspresi lambda, fungsi lokal, atau metode. Jika pabrik asinkron, dan Anda menggunakan Task<TResult>.Result, ini akan menyebabkan kebuntuan.

Anti-pattern: Deadlock with async factory. Do not copy!

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

Anti-pattern: Deadlock with async factory inner issue. Do not copy!

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 anti-pola ini dan kebuntuan terjadi, Anda dapat melihat dua utas yang menunggu dari jendela Tumpukan Paralel Visual Studio. Untuk informasi selengkapnya, lihat Menampilkan utas dan tugas di jendela Tumpukan Paralel.

Dependensi tawanan

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

Anti-pattern: Captive dependency. Do not copy!

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 Bar objek, dan karena Foo merupakan singleton, dan Bar terlingkup - ini adalah kesalahan konfigurasi. Apa adanya, Foo hanya akan dibuat sekali, dan itu akan berpegang pada Bar untuk masa pakainya, yang lebih lama dari masa pakai cakupan yang dimaksudkan.Bar Anda harus mempertimbangkan untuk memvalidasi cakupan, dengan meneruskan validateScopes: true ke BuildServiceProvider(IServiceCollection, Boolean). Saat memvalidasi cakupan, Anda akan mendapatkan InvalidOperationException pesan yang mirip dengan "Tidak dapat menggunakan layanan terlingkup 'Bar' dari singleton 'Foo'.".

Untuk informasi selengkapnya, lihat Validasi cakupan.

Layanan terlingkup sebagai singleton

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

Anti-pattern: Scoped service becomes singleton. Do not copy!

Dalam kode sebelumnya, Bar diambil dalam IServiceScope, yang benar. Anti-pola adalah pengambilan di Bar luar cakupan, dan variabel diberi nama avoid untuk menunjukkan contoh pengambilan yang salah.

Lihat juga