Udostępnij przez


Wstrzykiwanie zależności platformy .NET

Platforma .NET obsługuje wzorzec projektowania oprogramowania iniekcji zależności (DI), który jest techniką osiągnięcia inwersji kontroli (IoC) między klasami i ich zależnościami. Wstrzykiwanie zależności na platformie .NET jest wbudowaną częścią struktury wraz z konfiguracją, rejestrowaniem i wzorcem opcji.

Zależność to obiekt, od którego zależy inny obiekt. Poniższa MessageWriter klasa ma metodę Write , od których mogą zależeć inne klasy:

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

Klasa może utworzyć wystąpienie MessageWriter klasy, aby użyć jej Write metody. W poniższym przykładzie klasa MessageWriter jest zależnością klasy Worker:

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);
        }
    }
}

W tym przypadku klasa Worker jest tworzona i bezpośrednio zależy od klasy MessageWriter. Zakodowane na sztywno zależności, takie jak te, są problematyczne i należy unikać z następujących powodów:

  • Aby zastąpić MessageWriter inną implementacją, należy zmodyfikować klasę Worker .
  • Jeśli MessageWriter ma zależności, Worker klasa musi również je skonfigurować. W dużym projekcie z wieloma klasami zależnymi od MessageWriter kod konfiguracji staje się rozpraszany po całej aplikacji.
  • Ta implementacja jest trudna do testowania jednostkowego. Aplikacja powinna używać makiety lub klasy wycinkowej MessageWriter , która nie jest możliwa w przypadku tego podejścia.

Koncepcja

Wstrzykiwanie zależności rozwiązuje problemy z zakodowanymi zależnościami za pomocą:

  • Użycie interfejsu lub klasy bazowej do abstrakcji implementacji zależności.

  • Rejestracja zależności w kontenerze usługi.

    Platforma .NET udostępnia wbudowany kontener IServiceProviderusługi . Usługi są zwykle rejestrowane podczas uruchamiania aplikacji i dołączane do elementu IServiceCollection. Po dodaniu wszystkich usług użyj polecenia BuildServiceProvider , aby utworzyć kontener usługi.

  • Iniekcja usługi do konstruktora klasy, w której jest używana.

    Struktura bierze na siebie odpowiedzialność za utworzenie wystąpienia zależności i usunięcie go, gdy nie jest już potrzebne.

Wskazówka

W terminologii iniekcji zależności usługa jest zazwyczaj obiektem, który zapewnia usługę innym obiektom, takim jak IMessageWriter usługa. Usługa nie jest powiązana z usługą internetową, chociaż może używać usługi internetowej.

Załóżmy na przykład, że IMessageWriter interfejs definiuje metodę Write . Ten interfejs jest implementowany przez konkretny typ , MessageWriterpokazany wcześniej. Poniższy przykładowy kod rejestruje usługę IMessageWriter przy użyciu konkretnego typu MessageWriter. Metoda AddSingleton rejestruje usługę z pojedynczym okresem istnienia, co oznacza, że nie jest usuwana, dopóki aplikacja nie zostanie zamknięta.

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>

W poprzednim przykładzie kodu wyróżnione wiersze:

  • Utwórz instancję kreatora aplikacji hosta.
  • Skonfiguruj usługi, rejestrując Worker jako usługę hostowaną i IMessageWriter interfejs jako pojedynczą usługę z odpowiednią implementacją MessageWriter klasy.
  • Skompiluj hosta i uruchom go.

Host zawiera dostawcę usługi wstrzykiwania zależności. Zawiera również wszystkie pozostałe odpowiednie usługi wymagane do automatycznego utworzenia wystąpienia elementu i dostarczenia odpowiedniej Worker implementacji IMessageWriter jako argumentu.

Korzystając ze wzorca DI, usługa robocza nie używa konkretnego typu MessageWriter, a jedynie interfejsu IMessageWriter, który implementuje. Ten projekt ułatwia zmianę implementacji używanej przez usługę roboczą bez jej modyfikowania. Usługa robocza również nie tworzy wystąpieniaMessageWriter. Kontener DI tworzy wystąpienie.

Teraz wyobraź sobie, że chcesz zamienić MessageWriter na typ korzystający z platformowej usługi rejestrowania. Utwórz klasę LoggingMessageWriter, która zależy od ILogger<TCategoryName>, otrzymując ją poprzez konstruktor.

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

Aby przełączyć się z MessageWriter do LoggingMessageWriter, po prostu zaktualizuj wywołanie AddSingleton, aby zarejestrować tę nową implementację IMessageWriter:

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

Wskazówka

Kontener rozwiązuje problem ILogger<TCategoryName> dzięki wykorzystaniu (ogólnych) typów otwartych, co eliminuje konieczność rejestrowania każdego typu skonstruowanego (ogólnego).

Zachowanie iniekcji konstruktora

Usługi można rozwiązać przy użyciu IServiceProvider(wbudowanego kontenera usług) lub ActivatorUtilities. ActivatorUtilities tworzy obiekty, które nie są zarejestrowane w kontenerze i są używane z niektórymi funkcjami struktury.

Konstruktory mogą akceptować argumenty, które nie są dostarczane przez iniekcję zależności, ale argumenty muszą przypisywać wartości domyślne.

W przypadku rozwiązywania usług przez IServiceProvider lub ActivatorUtilities, iniekcja konstruktora wymaga publicznego konstruktora.

Gdy ActivatorUtilities służy do rozpoznawania usług, iniekcja konstruktora wymaga, aby istniał tylko jeden odpowiedni konstruktor. Przeciążenia konstruktora są obsługiwane, ale tylko jedno przeciążenie może istnieć, którego argumenty mogą być spełnione przez wstrzykiwanie zależności.

Reguły wyboru konstruktora

Gdy typ definiuje więcej niż jeden konstruktor, dostawca usług ma logikę określania, który konstruktor ma być używany. Konstruktor z największymi parametrami, w których wybierane są typy di-rozpoznawania. Rozważmy następującą przykładową usługę:

public class ExampleService
{
    public ExampleService()
    {
    }

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

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

W poprzednim kodzie załóżmy, że logowanie zostało dodane i jest możliwe do zidentyfikowania przez usługodawcę, ale typy ServiceA i ServiceB nie są dostępne. Konstruktor z parametrem ILogger<ExampleService> rozpoznaje ExampleService wystąpienie. Mimo że istnieje konstruktor, który definiuje więcej parametrów, typy ServiceA i ServiceB nie mogą być rozwiązane przez Dependency Injection (DI).

Jeśli podczas odnajdywania konstruktorów występuje niejednoznaczność, zgłaszany jest wyjątek. Rozważmy następującą przykładową usługę w języku C#:

Ostrzeżenie

Ten ExampleService kod z niejednoznacznymi parametrami typu rozwiązywalnymi przez DI zgłasza wyjątek. Nie rób tego — ma to na celu pokazanie, co oznacza "niejednoznaczne typy rozwiązywalne przez DI".

public class ExampleService
{
    public ExampleService()
    {
    }

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

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

W poprzednim przykładzie istnieją trzy konstruktory. Pierwszy konstruktor jest bez parametrów i nie wymaga żadnych usług od dostawcy usług. Załóżmy, że zarówno rejestrowanie, jak i opcje zostały dodane do kontenera DI i są usługami rozpoznawania di. Gdy kontener DI próbuje rozpoznać ExampleService typ, zgłasza wyjątek, ponieważ dwa konstruktory są niejednoznaczne.

Unikaj niejednoznaczności, definiując konstruktor, który akceptuje oba typy rozpoznawania di zamiast tego:

public class ExampleService
{
    public ExampleService()
    {
    }

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

Walidacja zakresu

Usługi ograniczone zakresem są usuwane z pamięci przez kontener, który je utworzył. Jeśli usługa o określonym zakresie jest tworzona w kontenerze głównym, czas życia usługi jest skutecznie promowany do singleton, ponieważ kontener główny usuwa ją tylko po zamknięciu aplikacji. Sprawdzanie poprawności zakresów usługi przechwytuje te sytuacje, gdy BuildServiceProvider jest wywoływana.

Gdy aplikacja jest uruchamiana w środowisku programistycznym i wywołuje polecenie CreateApplicationBuilder w celu skompilowania hosta, domyślny dostawca usług przeprowadza kontrole w celu sprawdzenia, czy:

  • Usługi o określonym zakresie nie są rozpoznawane przez głównego dostawcę usług.
  • Usługi o określonym zakresie nie są wstrzykiwane do pojedynczychtonów.

Scenariusze zakresu

Element IServiceScopeFactory jest zawsze rejestrowany jako pojedynczy, ale IServiceProvider może się różnić w zależności od okresu istnienia zawierającej klasy. Jeśli na przykład wywołujesz usługi z zakresu i któraś z tych usług przyjmuje parametr IServiceProvider, jest to instancja o określonym zakresie.

Aby osiągnąć usługi określania zakresu w ramach implementacji IHostedServiceprogramu , takich jak BackgroundService, nie wprowadzaj zależności usługi za pomocą iniekcji konstruktora. Zamiast tego należy wstrzyknąć IServiceScopeFactory, utworzyć zakres, a następnie rozwiązać zależności z zakresu, aby użyć odpowiedniego okresu istnienia usługi.

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);
                }
            }
        }
    }
}

W poprzednim kodzie, gdy aplikacja jest uruchomiona, usługa w tle:

  • Zależy od .IServiceScopeFactory
  • Tworzy element IServiceScope do rozwiązywania pozostałych usług.
  • Rozwiązuje zakres usług do użycia.
  • Działa na temat przetwarzania obiektów, a następnie przekazywania ich, a na koniec oznacza je jako przetworzone.

W przykładowym kodzie źródłowym można zobaczyć, jak implementacje IHostedService mogą korzystać z okresów istnienia usługi w zakresie.

Usługi kluczy

Możesz rejestrować usługi i wykonywać wyszukiwania na podstawie klucza. Innymi słowy, istnieje możliwość zarejestrowania wielu usług przy użyciu różnych kluczy i użycia tego klucza do wyszukiwania.

Rozważmy na przykład przypadek, w którym istnieją różne implementacje interfejsu IMessageWriter: MemoryMessageWriter i QueueMessageWriter.

Te usługi można zarejestrować przy użyciu przeciążenia metod rejestracji usługi (widocznych wcześniej), które obsługują klucz jako parametr:

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

Element key nie jest ograniczony do string. key może być dowolnym object, o ile typ poprawnie implementuje Equals.

W konstruktorze klasy używającej IMessageWriterklasy należy dodać element FromKeyedServicesAttribute , aby określić klucz usługi do rozpoznania:

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

Właściwość KeyedService.AnyKey

Właściwość KeyedService.AnyKey udostępnia specjalny klucz do pracy z usługami opartymi na kluczach. Usługę można zarejestrować przy użyciu KeyedService.AnyKey jako zapasowego, która pasuje do dowolnego klucza. Jest to przydatne, gdy chcesz podać domyślną implementację dla dowolnego klucza, który nie ma jawnej rejestracji.

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}");

W poprzednim przykładzie:

  • Żądanie ICache z użyciem klucza "premium" zwraca wystąpienie PremiumCache.
  • ICache Żądanie z dowolnym innym kluczem (na przykład "basic" lub "standard") powoduje utworzenie nowego DefaultCache przy użyciu rezerwowego AnyKey.

Ważne

Począwszy od platformy .NET 10, wywołanie GetKeyedService() z KeyedService.AnyKey zgłasza wyjątkiem InvalidOperationException, ponieważ AnyKey jest przeznaczone jako mechanizm domyślnej rejestracji, a nie jako klucz do zapytania. Aby uzyskać więcej informacji, zobacz Rozwiązywanie problemów w funkcji GetKeyedService() i GetKeyedServices() przy użyciu elementu AnyKey.

Zobacz także