Sdílet prostřednictvím


Injektáž závislostí .NET

.NET podporuje model návrhu softwaru injektáž závislostí (DI), což je technika pro dosažení inverze řízení (IoC) mezi třídami a jejich závislostmi. Injektáž závislostí v .NET je integrovaná součást architektury spolu s konfigurací, protokolováním a vzorem možností.

Závislost je objekt, na který závisí jiný objekt. Následující MessageWriter třída má metodu Write , na které můžou záviset jiné třídy:

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

Třída může vytvořit instanci MessageWriter třídy pro použití její Write metody. V následujícím příkladu je třída MessageWriterzávislostí třídy 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);
        }
    }
}

V tomto případě Worker třída vytvoří a přímo závisí na MessageWriter třídě. Takto zakódované závislosti jsou problematické a měly by se jim vyhnout z následujících důvodů:

  • Chcete-li nahradit MessageWriter jinou implementací, je nutné upravit Worker třídu.
  • Pokud MessageWriter obsahuje závislosti, Worker třída je také musí nakonfigurovat. Ve velkém projektu s více třídami v závislosti na MessageWritertom se konfigurační kód rozdělí v aplikaci.
  • Tato implementace je obtížná pro testování jednotek. Aplikace by měla používat napodobení nebo třídu zástupných procedur MessageWriter , která s tímto přístupem není možná.

Koncept

Injektáž závislostí řeší pevně zakódované problémy závislostí prostřednictvím:

  • Použití rozhraní nebo základní třídy k abstrakci implementace závislostí.

  • Registrace závislosti v kontejneru služby

    .NET poskytuje integrovaný kontejner služby . IServiceProvider Služby se obvykle registrují v start-upu aplikace a připojí se k sadě IServiceCollection. Po přidání všech služeb použijte BuildServiceProvider k vytvoření kontejneru služby.

  • Injekce služby do konstruktoru třídy, ve které se používá.

    Architektura přebírá odpovědnost za vytvoření instance závislosti a její likvidaci, když už ji nepotřebujete.

Návod

V terminologii injektáže závislostí je služba obvykle objekt, který poskytuje službu jiným objektům, jako je IMessageWriter služba. Služba nesouvisí s webovou službou, i když může používat webovou službu.

Předpokládejme například, že IMessageWriter rozhraní definuje metodu Write . Toto rozhraní je implementováno konkrétním typem, MessageWriterkterý je znázorněn dříve. Následující ukázkový kód zaregistruje IMessageWriter službu s konkrétním typem MessageWriter. Metoda AddSingleton zaregistruje službu s singletonovou životností, což znamená, že nedojde k jejímu uvolnění, dokud se aplikace nevypne.

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>

V předchozím příkladu kódu zvýrazněné řádky:

  • Vytvořte instanci tvůrce hostitelských aplikací.
  • Nakonfigurujte služby registrací Workerhostované služby a IMessageWriter rozhraní jako singletonové služby s odpovídající implementací třídy MessageWriter.
  • Sestavte host a spusťte ho.

Hostitel obsahuje poskytovatele služby injektáže závislostí. Obsahuje také všechny ostatní relevantní služby potřebné k automatickému vytvoření instance Worker a poskytnutí odpovídající IMessageWriter implementace jako argumentu.

Pomocí vzoru DI nepoužívá pracovní služba konkrétní typ MessageWriter, pouze IMessageWriter rozhraní, které implementuje. Tento návrh usnadňuje změnu implementace, kterou používá pracovní služba, beze změny pracovní služby. Pracovní služba také nevytvoří tuto instanciMessageWriter. Kontejner DI vytvoří instanci.

Teď si představte, že chcete vyměnit MessageWriter za typ, který používá službu protokolování poskytovanou frameworkem. Vytvořte třídu LoggingMessageWriter, která závisí na ILogger<TCategoryName> tím, že ji požaduje v konstruktoru.

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

Chcete-li přejít z MessageWriter na LoggingMessageWriter, jednoduše aktualizujte volání funkcí, aby došlo k registraci této nové implementace AddSingleton:

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

Návod

Kontejner vyřeší ILogger<TCategoryName> využitím (generických) otevřených typů, což odstraňuje potřebu registrovat každý (generický) vytvořený typ.

Chování injektáže konstruktoru

Služby lze vyřešit pomocí IServiceProvider (integrovaného kontejneru služby) nebo ActivatorUtilities. ActivatorUtilities vytvoří objekty, které nejsou zaregistrované v kontejneru a používají se s některými funkcemi architektury.

Konstruktory mohou přijímat argumenty, které nejsou poskytovány injektáží závislostí, ale argumenty musí přiřadit výchozí hodnoty.

Při vyvolávání služeb pomocí IServiceProvider nebo ActivatorUtilities vyžaduje injektáž konstruktoru veřejný konstruktor.

Při ActivatorUtilities rezolvování služeb injektování konstruktoru vyžaduje, aby existoval pouze jeden použitelný konstruktor. Přetížení konstruktoru jsou podporována, ale pouze jedno přetížení může existovat, jehož argumenty mohou být splněny injektáž závislostí.

Pravidla výběru konstruktoru

Pokud typ definuje více než jeden konstruktor, poskytovatel služeb má logiku pro určení konstruktoru, který se má použít. Konstruktor s nejvíce parametry, ve kterých jsou typy di-přeložitelné, je vybrán. Představte si následující ukázkovou službu:

public class ExampleService
{
    public ExampleService()
    {
    }

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

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

V předchozím kódu předpokládejme, že bylo přidáno protokolování, které lze vyřešit od poskytovatele služeb, ale typy ServiceA a ServiceB nejsou. Konstruktor s parametrem ILogger<ExampleService> vyřeší instanci ExampleService. I když existuje konstruktor, který definuje více parametrů, ServiceA a ServiceB typy nelze vyřešit DI.

Pokud při zjišťování konstruktorů existuje nejednoznačnost, vyvolá se výjimka. Představte si následující ukázkovou službu jazyka C#:

Výstraha

Tento ExampleService kód s nejednoznačnými parametry typu DI vyvolá výjimku. Nedělejte to – účelem je ukázat, co znamená "nejednoznačné DI-resolvovatelné typy".

public class ExampleService
{
    public ExampleService()
    {
    }

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

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

V předchozím příkladu existují tři konstruktory. První konstruktor je bez parametrů a nevyžaduje žádné služby od poskytovatele služeb. Předpokládejme, že do kontejneru DI byly přidány protokolování i možnosti a že se jedná o služby, které lze přeložit. Když se kontejner DI pokusí vyřešit typ ExampleService, vyvolá výjimku, protože dva konstruktory jsou nejednoznačné.

Vyhněte se nejednoznačnosti definováním konstruktoru, který místo toho přijímá oba typy překladu DI:

public class ExampleService
{
    public ExampleService()
    {
    }

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

Ověření oboru

Služby s omezeným dosahem jsou odstraněny kontejnerem, který je vytvořil. Pokud se v kořenovém kontejneru vytvoří vymezená služba, životnost služby se efektivně zvýší na singleton , protože je odstraněna pouze kořenovým kontejnerem, když se aplikace vypne. Ověřování rozsahů služby tyto situace zachytí, když BuildServiceProvider je volána.

Když se aplikace spustí ve vývojovém prostředí a zavolá createApplicationBuilder k sestavení hostitele, výchozí poskytovatel služeb provede kontroly, aby ověřil, že:

  • Služby s vymezeným oborem se od poskytovatele kořenových služeb nepřeloží.
  • Služby s vymezeným oborem se nevkážou do singletonů.

Scénáře oborů

Je IServiceScopeFactory vždy registrován jako singleton, ale IServiceProvider může se lišit v závislosti na životnosti obsahující třídy. Pokud například přeložíte služby z oboru a každá z těchto služeb převezme IServiceProvider, jedná se o instanci s vymezeným oborem.

Pokud chcete dosáhnout rozsahu služeb v rámci implementací IHostedService, jako je například BackgroundService, nepřidávejte závislosti služby prostřednictvím injektáže konstruktoru. Místo toho vložte IServiceScopeFactory, vytvořte obor a pak přeložte závislosti z oboru tak, aby používala odpovídající dobu životnosti služby.

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

V předchozím kódu, zatímco je aplikace spuštěná, služba na pozadí:

  • Závisí na závislosti IServiceScopeFactoryna .
  • Vytvoří IServiceScope pro řešení jiných služeb.
  • Řeší služby s vymezeným oborem pro spotřebu.
  • Pracuje na zpracování objektů a jejich následné předávání a nakonec je označí jako zpracované.

Z ukázkového zdrojového kódu si můžete prohlédnout, jak můžou implementace těžit z rozsahu IHostedService životnosti služby.

Služby s klíči

Služby můžete registrovat a vyhledávat na základě klíče. Jinými slovy, je možné zaregistrovat více služeb s různými klíči a tento klíč použít pro vyhledávání.

Představte si například případ, kdy máte různé implementace rozhraní IMessageWriter: MemoryMessageWriter a QueueMessageWriter.

Tyto služby můžete zaregistrovat pomocí přetížení metod registrace služby (vidět dříve), které podporují klíč jako parametr:

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

Není key omezen na string. Může key být libovolný object , pokud typ správně implementuje Equals.

V konstruktoru třídy, která používá IMessageWriter, přidáte FromKeyedServicesAttribute k zadání klíče služby k vyřešení:

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

KeyedService.AnyKey – vlastnost

Vlastnost KeyedService.AnyKey poskytuje speciální klíč pro práci se službami identifikovanými klíčem. Službu můžete zaregistrovat pomocí KeyedService.AnyKey jako záložní řešení, které odpovídá jakémukoli klíči. To je užitečné, když chcete poskytnout výchozí implementaci pro jakýkoli klíč, který nemá explicitní registraci.

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

V předchozím příkladu:

  • Požadavek ICache, který využívá klíč "premium", vrátí instanci PremiumCache.
  • Žádost o ICache s použitím jakéhokoli jiného klíče (například "basic" nebo "standard") vytvoří nový DefaultCache s použitím záložního AnyKey.

Důležité

Od verze .NET 10 vyvolá volání GetKeyedService() s KeyedService.AnyKey výjimku InvalidOperationException, protože AnyKey je určen jako záložní registrace, ne jako klíč dotazu. Další informace najdete v tématu Oprava problémů v GetKeyedService() a GetKeyedServices() s anyKey.

Viz také