Dependency injection adalah teknik menyediakan dependensi sebuah kelas dari luar, sehingga kelas itu tidak membuat sendiri objek yang dibutuhkannya. Keluarga pustaka Microsoft.Extensions.DependencyInjection menyediakan container, yaitu wadah yang menyimpan daftar service, menyusun objek beserta dependensinya, dan mengatur masa hidup setiap service. Topik ini membahas registrasi service, resolusi dari container, tiga masa hidup bawaan, dan penyusunan objek otomatis melalui constructor injection, dengan demo yang keluarannya dapat direproduksi sepenuhnya.
Konsep dependency injection#
Kelas yang membuat sendiri dependensinya terikat kuat pada implementasi konkret tersebut. Kelas pelapor yang menuliskan new PenyimpanMemori() di dalam konstruktornya tidak dapat berpindah ke penyimpanan lain tanpa diedit, dan pengujian atasnya tidak dapat mengganti penyimpanan dengan versi tiruan. Dependency injection membalik arah itu, yaitu dependensi dinyatakan sebagai antarmuka pada parameter constructor, lalu objek konkret disediakan dari luar oleh pihak yang menyusun kelasnya. Kelas yang bergantung pada beberapa layanan sekaligus pun cukup mendaftarkan semuanya sebagai parameter constructor tanpa membedakan cara pembuatannya.
Pola ini disebut inversion of control karena keputusan pemilihan implementasi berpindah dari kelas pemakai ke tempat penyusunan objeknya. Manfaat langsungnya muncul pada pengujian dan penggantian komponen, yaitu implementasi asli digantikan registrasi lain tanpa perubahan kode pemakainya, karena kontraktanya tetap antarmuka yang sama. Seluruh kerangka kerja .NET modern, termasuk ASP.NET Core, membangun layanannya di atas pola ini, sehingga penguasaan konsepnya menjadi bekal untuk hampir semua aplikasi nyata.
Mendaftarkan service dan membangun container#
Registrasi dilakukan pada objek ServiceCollection, dan setiap panggilan Add mengaitkan satu service dengan masa hidupnya. Metode generik AddSingleton<TService, TImplementation> dan kedua saudaranya mendaftarkan pasangan antarmuka dan implementasinya, sedangkan bentuk satu argumen seperti AddSingleton<InstansiSingleton>() mendaftarkan kelas konkret sebagai service untuk dirinya sendiri. Panggilan BuildServiceProvider menyusun registrasi itu menjadi IServiceProvider yang siap memenuhi permintaan service, dan resolusi dikerjakan pada saat permintaan terjadi, bukan pada saat container dibangun.
// tidak-dijalankan
// Contoh: GetService mengembalikan null, sedangkan GetRequiredService melempar exception
IPenyimpanSuhu? mungkin = container.GetService<IPenyimpanSuhu>();
IPenyimpanSuhu pasti = container.GetRequiredService<IPenyimpanSuhu>();Dua metode resolusinya berbeda perilaku pada service yang tidak terdaftar. Metode GetService mengembalikan null sehingga pemakainya memeriksa hasilnya, sedangkan GetRequiredService melempar InvalidOperationException dengan pesan yang menyebut service yang tidak terdaftar, dan pilihan kedua ini lazim dipakai karena kegagalannya terjadi cepat dan jelas. Container yang dibangun mengimplementasikan IDisposable, sehingga pemanggilan Dispose atasnya, baik lewat pernyataan using maupun di akhir aplikasi, ikut melepas service yang mengharuskan pelepasan.
Tiga masa hidup service#
Masa hidup (lifetime) menentukan berapa lama satu instansi service dipakai ulang, dan pustaka ini menyediakan tiga pilihan bawaan. Perilakunya dirangkum pada tabel berikut.
| Masa hidup | Metode registrasi | Instansi yang diberikan |
|---|---|---|
| Singleton | AddSingleton | Satu instansi untuk seluruh masa aplikasi |
| Scoped | AddScoped | Satu instansi per lingkup (scope) |
| Transient | AddTransient | Instansi baru pada setiap permintaan |
Lingkup dibuat dengan CreateScope() atas container, dan lingkup itu sendiri dapat dibuang sehingga service bertipe IDisposable di dalamnya ikut dilepas. Pada aplikasi web, satu lingkup dibuat untuk setiap permintaan HTTP secara otomatis, sehingga service yang menyimpan keadaan sepanjang satu permintaan didaftarkan Scoped, service yang mahal dibuatnya dan aman dibagikan didaftarkan Singleton, sedangkan service ringan yang tidak boleh dibagikan didaftarkan Transient. Instansi Singleton dibuat malas pada permintaan pertamanya, dan satu instansi itu dipakai ulang sampai container dibuang.
Demo container dan constructor injection#
Program berikut mendaftarkan tiga kelas contoh dengan masa hidup berbeda, lalu mengurutkan nomor instansinya. Setiap instansi menerima nomor urut dari satu penghitung bersama pada saat dibuat, sehingga nomor yang sama menandakan instansi yang sama pula. Di bagian akhir, kelas PenghitungRataRata dinyatakan bergantung pada IPenyimpanSuhu melalui constructornya, dan container menyusun keduanya tanpa pemanggilan new secara manual.
#:package Microsoft.Extensions.DependencyInjection@10.0.0
// Program: mendaftarkan service, menguji lifetime, dan memakai constructor injection
using Microsoft.Extensions.DependencyInjection;
var layanan = new ServiceCollection();
layanan.AddSingleton<InstansiSingleton>();
layanan.AddScoped<InstansiScoped>();
layanan.AddTransient<InstansiTransient>();
layanan.AddSingleton<IPenyimpanSuhu, PenyimpanMemori>();
layanan.AddTransient<PenghitungRataRata>();
using ServiceProvider container = layanan.BuildServiceProvider();
InstansiSingleton a = container.GetRequiredService<InstansiSingleton>();
InstansiSingleton b = container.GetRequiredService<InstansiSingleton>();
InstansiTransient t1 = container.GetRequiredService<InstansiTransient>();
InstansiTransient t2 = container.GetRequiredService<InstansiTransient>();
Console.WriteLine($"Singleton : {a.Nomor} lalu {b.Nomor}");
Console.WriteLine($"Transient : {t1.Nomor} lalu {t2.Nomor}");
using (IServiceScope lingkupSatu = container.CreateScope())
{
InstansiScoped s1 = lingkupSatu.ServiceProvider.GetRequiredService<InstansiScoped>();
InstansiScoped s2 = lingkupSatu.ServiceProvider.GetRequiredService<InstansiScoped>();
Console.WriteLine($"Lingkup 1 : {s1.Nomor} lalu {s2.Nomor}");
}
using (IServiceScope lingkupDua = container.CreateScope())
{
InstansiScoped s = lingkupDua.ServiceProvider.GetRequiredService<InstansiScoped>();
Console.WriteLine($"Lingkup 2 : {s.Nomor} lalu {s.Nomor}");
}
// container menyusun PenghitungRataRata beserta dependensinya secara otomatis
PenghitungRataRata penghitung = container.GetRequiredService<PenghitungRataRata>();
penghitung.Catat("Lab", 24.5);
penghitung.Catat("Kantor", 26.0);
penghitung.Catat("Lab", 25.5);
Console.WriteLine($"Rata-rata Lab: {penghitung.RataRata("Lab"):F1}");
// penghitung nomor urut instansi yang dipakai ketiga kelas contoh
static class PenghitungInstansi
{
public static int Berikutnya() => ++nomor;
private static int nomor;
}
class InstansiSingleton { public int Nomor { get; } = PenghitungInstansi.Berikutnya(); }
class InstansiScoped { public int Nomor { get; } = PenghitungInstansi.Berikutnya(); }
class InstansiTransient { public int Nomor { get; } = PenghitungInstansi.Berikutnya(); }
interface IPenyimpanSuhu
{
void Catat(string ruang, double suhu);
IReadOnlyList<double> Baca(string ruang);
}
class PenyimpanMemori : IPenyimpanSuhu
{
private readonly Dictionary<string, List<double>> data = new();
public void Catat(string ruang, double suhu)
{
if (!data.TryGetValue(ruang, out List<double>? barisan))
{
barisan = new List<double>();
data[ruang] = barisan;
}
barisan.Add(suhu);
}
public IReadOnlyList<double> Baca(string ruang) => data[ruang];
}
class PenghitungRataRata
{
private readonly IPenyimpanSuhu penyimpan;
public PenghitungRataRata(IPenyimpanSuhu penyimpan) => this.penyimpan = penyimpan;
public void Catat(string ruang, double suhu) => penyimpan.Catat(ruang, suhu);
public double RataRata(string ruang) => penyimpan.Baca(ruang).Average();
}Jalankan program tersebut dari folder topik dengan perintah berikut.
# aktifkan .NET yang terpasang di sandbox
export DOTNET_ROOT=/home/kal/.dotnet PATH=/home/kal/.dotnet:$PATH DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1
# jalankan program contoh dari folder topik
dotnet run --file Program.csSingleton : 1 lalu 1
Transient : 2 lalu 3
Lingkup 1 : 4 lalu 4
Lingkup 2 : 5 lalu 5
Rata-rata Lab: 25.0Setiap baris keluaran mengonfirmasi satu aturan masa hidup. Dua resolusi Singleton menghasilkan nomor 1 dan 1 karena instansinya dipakai ulang, dua resolusi Transient menghasilkan 2 dan 3 karena setiap permintaan membuat instansi baru, dan dua resolusi dalam lingkup pertama menghasilkan 4 dan 4, sedangkan lingkup kedua mendapat instansi sendiri dengan nomor 5. Baris terakhir menunjukkan constructor injection, yaitu PenghitungRataRata menerima PenyimpanMemori lewat constructornya karena keduanya terdaftar, dan program tidak pernah memanggil new PenghitungRataRata(...) sama sekali.
Kesalahan umum#
- Mendaftarkan Singleton yang bergantung pada service Scoped. Instansi tunggal itu menyandera instansi lingkup yang pertama kali menyusunnya (captive dependency), sehingga keadaan service lingkup itu bocor ke seluruh aplikasi, dan perbaikannya adalah membalik arah ketergantungan atau menurunkan masa hidup pemandunya.
- Memakai service tanpa mendaftarkannya. Resolusi lewat
GetRequiredServicemelemparInvalidOperationExceptionyang menyebut tipenya, sehingga pesan error tersebut ditelusuri ke daftar registrasi padaServiceCollection. - Membangun container berkali-kali. Setiap panggilan
BuildServiceProvidermenghasilkan wadah baru dengan singleton-nya sendiri, sehingga container dibangun satu kali pada titik masuk aplikasi dan diteruskan ke seluruh bagian yang membutuhkannya.
Ringkasan#
- Dependensi dinyatakan sebagai antarmuka pada constructor, dan container menyediakan implementasinya sehingga kelas pemakai bebas dari pembuatan objek sendiri.
- Registrasi pada
ServiceCollectionmengaitkan pasangan service dan implementasi dengan masa hidupnya, laluBuildServiceProvidermenghasilkan container yang dapat dibuang. - Singleton berbagi satu instansi untuk seluruh aplikasi, Scoped per lingkup, dan Transient untuk setiap permintaan, dengan lingkup dibuat melalui
CreateScope(). - Constructor injection menyusun grafik objek secara otomatis, dan kegagalan registrasi terdeteksi cepat melalui
GetRequiredService.
Referensi#
- Dependency injection in .NET — Microsoft Learn — Panduan resmi dependency injection .NET, mulai dari registrasi sampai resolusi service.
- Dependency injection in .NET: Service lifetimes — Microsoft Learn — Penjelasan tiga masa hidup service beserta contoh registrasinya.
- ServiceCollection class — Microsoft Learn — Rujukan API kumpulan registrasi service.