Bagikan melalui


Injeksi dependensi .NET

.NET mendukung pola desain perangkat lunak injeksi dependensi (DI), yang merupakan teknik untuk mencapai Inversion of Control (IoC) antara kelas dan dependensinya. Injeksi dependensi di .NET adalah bagian bawaan dari kerangka kerja, bersama dengan konfigurasi, pengelogan, dan pola opsi.

Dependensi adalah objek yang bergantung pada objek lain. Kelas berikut MessageWriter memiliki metode Write yang mungkin bergantung dengan kelas lain.

public class MessageWriter : IMessageWriter
{
    public void Write(string message)
    {
        Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
    }
}

Sebuah kelas dapat membuat instance dari kelas MessageWriter untuk menggunakan metode Write miliknya. Dalam contoh berikut, MessageWriter kelas adalah dependensi dari Worker kelas :

public class Worker : BackgroundService
{
    private readonly MessageWriter _messageWriter = new();

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
            await Task.Delay(1_000, stoppingToken);
        }
    }
}

Dalam hal ini, kelas Worker membuat dan secara langsung tergantung pada kelas MessageWriter. Dependensi yang dikodekan secara permanen seperti ini bermasalah dan harus dihindari karena alasan berikut:

  • Untuk mengganti MessageWriter dengan implementasi yang berbeda, Anda harus memodifikasi Worker kelas.
  • Jika MessageWriter memiliki dependensi, Worker kelas juga harus mengonfigurasinya. Dalam proyek besar dengan beberapa kelas tergantung pada MessageWriter, kode konfigurasi menjadi tersebar di seluruh aplikasi.
  • Implementasi ini sulit untuk pengujian unit. Aplikasi harus menggunakan kelas tiruan atau stub MessageWriter , yang tidak dimungkinkan dengan pendekatan ini.

Konsep

Injeksi dependensi mengatasi masalah dependensi yang dikodekan secara permanen melalui:

  • Penggunaan antarmuka atau kelas dasar untuk mengabstraksi implementasi dependensi.

  • Pendaftaran dependensi dalam kontainer layanan.

    .NET menyediakan kontainer layanan bawaan, IServiceProvider. Layanan biasanya terdaftar di start-up aplikasi dan ditambahkan ke IServiceCollection. Setelah semua layanan ditambahkan, gunakan BuildServiceProvider untuk membuat kontainer layanan.

  • Penyuntikan layanan ke konstruktor dari kelas di mana layanan tersebut digunakan.

    Kerangka kerja mengambil tanggung jawab untuk membuat instans dependensi dan membuangnya ketika tidak lagi diperlukan.

Petunjuk / Saran

Dalam terminologi injeksi dependensi, layanan biasanya merupakan objek yang menyediakan layanan ke objek lain, seperti IMessageWriter layanan. Layanan ini tidak terkait dengan layanan web, meskipun mungkin menggunakan layanan web.

Sebagai contoh, asumsikan IMessageWriter antarmuka mendefinisikan Write metode . Antarmuka ini diimplementasikan oleh tipe konkret, MessageWriter, yang ditunjukkan sebelumnya. Kode sampel berikut mendaftarkan layanan IMessageWriter dengan tipe konkret MessageWriter. Metode ini AddSingleton mendaftarkan layanan dengan masa pakai singleton, yang berarti tidak dibuang sampai aplikasi dimatikan.

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHostedService<Worker>();
builder.Services.AddSingleton<IMessageWriter, MessageWriter>();

using IHost host = builder.Build();

host.Run();

// <SnippetMW>
public class MessageWriter : IMessageWriter
{
    public void Write(string message)
    {
        Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
    }
}
// </SnippetMW>

// <SnippetIMW>
public interface IMessageWriter
{
    void Write(string message);
}
// </SnippetIMW>

// <SnippetWorker>
public sealed class Worker(IMessageWriter messageWriter) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
            await Task.Delay(1_000, stoppingToken);
        }
    }
}

// </SnippetWorker>

Dalam contoh kode sebelumnya, baris yang disorot:

  • Buat instans pembangun aplikasi host.
  • Konfigurasi layanan dengan mendaftarkan Worker sebagai layanan yang dihosting, dan antarmuka sebagai layanan singleton dengan implementasi kelas IMessageWriter yang sesuai.
  • Siapkan host dan jalankan.

Host berisi penyedia layanan injeksi dependensi. Ini juga berisi semua layanan relevan lainnya yang diperlukan untuk secara otomatis membuat Worker instans dan memberikan implementasi yang sesuai IMessageWriter sebagai argumen.

Dengan menggunakan pola DI, layanan pekerja tidak menggunakan tipe MessageWriter konkret, hanya antarmuka IMessageWriter yang diimplementasikannya. Desain ini memudahkan untuk mengubah implementasi yang digunakan layanan pekerja tanpa memodifikasi layanan pekerja. Layanan pekerja juga tidak membuat instanceMessageWriter. Kontainer DI membuat instance.

Sekarang, bayangkan Anda ingin mengganti MessageWriter dengan tipe yang menggunakan layanan pengelogan yang disediakan framework. Buat kelas LoggingMessageWriter yang bergantung pada ILogger<TCategoryName> dengan memintanya di konstruktor.

public class LoggingMessageWriter(
    ILogger<LoggingMessageWriter> logger) : IMessageWriter
{
    public void Write(string message) =>
        logger.LogInformation("Info: {Msg}", message);
}

Untuk beralih dari MessageWriter ke LoggingMessageWriter, cukup perbarui panggilan ke AddSingleton untuk mendaftarkan implementasi baru IMessageWriter ini:

builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>();

Petunjuk / Saran

Kontainer menangani ILogger<TCategoryName> dengan memanfaatkan tipe terbuka (generik), yang menghapuskan kebutuhan untuk mendaftarkan setiap tipe terbangun (generik).

Perilaku injeksi konstruktor

Layanan dapat diselesaikan menggunakan IServiceProvider (kontainer layanan bawaan) atau ActivatorUtilities. ActivatorUtilities membuat objek yang tidak terdaftar dalam kontainer dan digunakan dengan beberapa fitur kerangka kerja.

Konstruktor dapat menerima argumen yang tidak disediakan oleh injeksi dependensi, tetapi argumen harus menetapkan nilai default.

Saat IServiceProvider atau ActivatorUtilities menyelesaikan layanan, injeksi konstruktor memerlukan konstruktor publik .

Ketika ActivatorUtilities menyelesaikan layanan, injeksi konstruktor mengharuskan hanya satu konstruktor yang berlaku. Kelebihan beban konstruktor didukung, tetapi hanya satu kelebihan beban yang dapat ada yang argumennya semuanya dapat dipenuhi oleh injeksi dependensi.

Aturan pemilihan konstruktor

Ketika jenis mendefinisikan lebih dari satu konstruktor, penyedia layanan memiliki logika untuk menentukan konstruktor mana yang akan digunakan. Konstruktor dengan parameter terbanyak di mana jenis dapat diselesaikan dipilih. Pertimbangkan contoh layanan berikut:

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(ILogger<ExampleService> logger)
    {
        // ...
    }

    public ExampleService(ServiceA serviceA, ServiceB serviceB)
    {
        // ...
    }
}

Dalam kode sebelumnya, asumsikan bahwa pengelogan telah ditambahkan dan dapat diselesaikan dari penyedia layanan tetapi ServiceA jenis dan ServiceB tidak. Konstruktor dengan ILogger<ExampleService> parameter menyelesaikan instans ExampleService . Meskipun ada konstruktor yang mendefinisikan lebih banyak parameter, ServiceA dan jenis ServiceB tidak dapat diselesaikan.

Jika ada ambiguitas saat menemukan konstruktor, pengecualian akan dilemparkan. Pertimbangkan layanan contoh C# berikut:

Peringatan

Kode ini ExampleService dengan parameter jenis yang dapat diselesaikan DI secara ambigu, melemparkan pengecualian. Jangan lakukan ini—hal ini dimaksudkan untuk menunjukkan maksud dari "jenis yang dapat diresolusikan secara ambigu dalam DI".

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(ILogger<ExampleService> logger)
    {
        // ...
    }

    public ExampleService(IOptions<ExampleOptions> options)
    {
        // ...
    }
}

Dalam contoh sebelumnya, ada tiga konstruktor. Konstruktor pertama tanpa parameter dan tidak memerlukan layanan dari penyedia layanan. Asumsikan bahwa pengelogan dan opsi telah ditambahkan ke kontainer DI dan merupakan layanan yang dapat diselesaikan. Ketika kontainer DI mencoba memperbaiki jenis ExampleService, kontainer DI melempar pengecualian, karena kedua konstruktor ambigu.

Hindari ambiguitas dengan menentukan konstruktor yang menerima tipe yang dapat diselesaikan melalui DI sebagai gantinya.

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(
        ILogger<ExampleService> logger,
        IOptions<ExampleOptions> options)
    {
        // ...
    }
}

Validasi cakupan

Layanan scoped dihapus oleh kontainer yang membuatnya. Jika layanan tercakup dibuat dalam kontainer akar, masa pakai layanan secara efektif dipromosikan ke singleton karena hanya dibuang oleh kontainer akar saat aplikasi dimatikan. Memvalidasi cakupan layanan menangkap situasi ini ketika BuildServiceProvider dipanggil.

Saat aplikasi berjalan di lingkungan pengembangan dan memanggil CreateApplicationBuilder untuk membangun host, penyedia layanan default melakukan pemeriksaan untuk memverifikasi bahwa:

  • Layanan terlingkup tidak diselesaikan dari penyedia layanan akar.
  • Layanan terlingkup tidak disuntikkan ke dalam singleton.

Skenario cakupan

IServiceScopeFactory selalu terdaftar sebagai singleton, tetapi IServiceProvider dapat bervariasi berdasarkan masa pakai kelas yang berisi. Misalnya, jika Anda menyelesaikan layanan dari cakupan, dan salah satu layanan tersebut mengambil IServiceProvider, itu adalah instans terlingkup.

Untuk mencapai layanan cakupan dalam implementasi IHostedService, seperti BackgroundService, jangan menyuntikkan dependensi layanan melalui injeksi konstruktor. Sebagai gantinya, masukkan IServiceScopeFactory, buat cakupan, lalu atasi dependensi dari cakupan untuk menggunakan masa pakai layanan yang sesuai.

namespace WorkerScope.Example;

public sealed class Worker(
    ILogger<Worker> logger,
    IServiceScopeFactory serviceScopeFactory)
    : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using (IServiceScope scope = serviceScopeFactory.CreateScope())
            {
                try
                {
                    logger.LogInformation(
                        "Starting scoped work, provider hash: {hash}.",
                        scope.ServiceProvider.GetHashCode());

                    var store = scope.ServiceProvider.GetRequiredService<IObjectStore>();
                    var next = await store.GetNextAsync();
                    logger.LogInformation("{next}", next);

                    var processor = scope.ServiceProvider.GetRequiredService<IObjectProcessor>();
                    await processor.ProcessAsync(next);
                    logger.LogInformation("Processing {name}.", next.Name);

                    var relay = scope.ServiceProvider.GetRequiredService<IObjectRelay>();
                    await relay.RelayAsync(next);
                    logger.LogInformation("Processed results have been relayed.");

                    var marked = await store.MarkAsync(next);
                    logger.LogInformation("Marked as processed: {next}", marked);
                }
                finally
                {
                    logger.LogInformation(
                        "Finished scoped work, provider hash: {hash}.{nl}",
                        scope.ServiceProvider.GetHashCode(), Environment.NewLine);
                }
            }
        }
    }
}

Dalam kode sebelumnya, saat aplikasi sedang berjalan, layanan latar belakang:

  • Tergantung pada IServiceScopeFactory.
  • Membuat IServiceScope untuk menyelesaikan layanan lain.
  • Menyelesaikan layanan terlingkup untuk dikonsumsi.
  • Bekerja pada pemrosesan objek dan kemudian menyampaikannya, dan akhirnya menandainya sebagai diproses.

Dari kode sumber sampel, Anda dapat melihat bagaimana implementasi IHostedService dapat memperoleh manfaat dari masa pakai layanan terlingkup.

Layanan utama

Anda dapat mendaftarkan layanan dan melakukan pencarian berdasarkan kunci. Dengan kata lain, Anda dapat mendaftarkan beberapa layanan dengan kunci yang berbeda dan menggunakan kunci ini untuk pencarian.

Misalnya, pertimbangkan kasus di mana Anda memiliki implementasi antarmuka IMessageWriteryang berbeda : MemoryMessageWriter dan QueueMessageWriter.

Anda dapat mendaftarkan layanan ini menggunakan kelebihan beban metode pendaftaran layanan (terlihat sebelumnya) yang mendukung kunci sebagai parameter:

services.AddKeyedSingleton<IMessageWriter, MemoryMessageWriter>("memory");
services.AddKeyedSingleton<IMessageWriter, QueueMessageWriter>("queue");

key tidak terbatas pada string. key Bisa menjadi apa pun yang object Anda inginkan, selama jenisnya mengimplementasikan Equalsdengan benar .

Di konstruktor kelas yang menggunakan IMessageWriter, Anda menambahkan FromKeyedServicesAttribute untuk menentukan kunci layanan untuk mengatasi:

public class ExampleService
{
    public ExampleService(
        [FromKeyedServices("queue")] IMessageWriter writer)
    {
        // Omitted for brevity...
    }
}

Properti AnyKey dari KeyedService

Properti KeyedService.AnyKey ini menyediakan kunci khusus untuk bekerja dengan layanan berkunci. Anda dapat mendaftarkan layanan menggunakan KeyedService.AnyKey sebagai fallback yang cocok dengan kunci apa pun. Ini berguna ketika Anda ingin memberikan implementasi default untuk kunci apa pun yang tidak memiliki pendaftaran eksplisit.

var services = new ServiceCollection();

// Register a fallback cache for any key.
services.AddKeyedSingleton<ICache>(KeyedService.AnyKey, (sp, key) =>
{
    // Create a cache instance based on the key.
    return new DefaultCache(key?.ToString() ?? "unknown");
});

// Register a specific cache for the "premium" key.
services.AddKeyedSingleton<ICache>("premium", new PremiumCache());

var provider = services.BuildServiceProvider();

// Requesting with "premium" key returns PremiumCache.
var premiumCache = provider.GetKeyedService<ICache>("premium");
Console.WriteLine($"Premium key: {premiumCache}");

// Requesting with any other key uses the AnyKey fallback.
var basicCache = provider.GetKeyedService<ICache>("basic");
Console.WriteLine($"Basic key: {basicCache}");

var standardCache = provider.GetKeyedService<ICache>("standard");
Console.WriteLine($"Standard key: {standardCache}");

Dalam contoh sebelumnya:

  • Meminta ICache dengan kunci "premium" akan mengembalikan instans PremiumCache.
  • Meminta ICache dengan kunci lain (seperti "basic" atau "standard") akan membuat DefaultCache baru menggunakan pengganti AnyKey.

Penting

Mulai dari .NET 10, memanggil GetKeyedService() dengan KeyedService.AnyKey akan melempar InvalidOperationException karena AnyKey dimaksudkan sebagai cadangan pendaftaran, bukan sebagai kunci pencarian. Untuk informasi selengkapnya, lihat Memperbaiki masalah di GetKeyedService() dan GetKeyedServices() dengan AnyKey.

Lihat juga