Megosztás:


.NET-függőséginjektálás

A .NET támogatja a függőséginjektálási (DI) szoftvertervezési mintát, amely az osztályok és függőségeik közötti vezérlési inverzió (IoC) elérésének technikája. A függőséginjektálás a .NET-ben a keretrendszer beépített része, a konfigurációval, a naplózással és a beállítási mintával együtt.

A függőségi egy objektum, amelytől egy másik objektum függ. Az alábbi MessageWriter osztály olyan metódussal Write rendelkezik, amelytől más osztályok függhetnek:

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

Egy osztály létrehozhat egy példányt a MessageWriter osztályból a Write metódusának használatához. Az alábbi példában az MessageWriter osztály az osztály függőségeWorker:

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

Ebben az esetben az Worker osztály létrehozza és közvetlenül az MessageWriter osztálytól függ. Az ehhez hasonló, szigorúan kódolt függőségek problémásak, ezért a következő okok miatt kerülendők:

  • Ha másik implementációra szeretne cserélni MessageWriter , módosítania kell az osztályt Worker .
  • Ha MessageWriter függőségek vannak, az Worker osztálynak is konfigurálnia kell őket. Egy nagy, MessageWriterfüggően több osztályt tartalmazó projektben a konfigurációs kód szétszóródik az alkalmazásban.
  • Ezt a megvalósítást nehéz egységtesztelni. Az alkalmazásnak egy mock vagy stub MessageWriter osztályt kell használnia, ami ezzel a megközelítéssel nem lehetséges.

A koncepció

A függőséginjektálás a következő módon oldja meg a nehezen kódolt függőségi problémákat:

  • Egy interfész vagy alaposztály használata a függőségi implementáció absztrakciójára.

  • A függőség regisztrálása egy szolgáltatástárolóban.

    A .NET egy beépített szolgáltatástárolót biztosít. IServiceProvider A szolgáltatások általában az alkalmazás indításakor vannak regisztrálva, és hozzá vannak fűzve egy IServiceCollection. Amint az összes szolgáltatást hozzáadták, használja a BuildServiceProvider a szolgáltatástároló létrehozásához.

  • A szolgáltatás injektálása annak az osztálynak a konstruktorába, ahol a szolgáltatást használják.

    A keretrendszer felelősséget vállal a függőség egy példányának létrehozásáért és eltávolításáért, ha már nincs rá szükség.

Jótanács

A függőséginjektálás terminológiájában a szolgáltatás általában olyan objektum, amely szolgáltatást nyújt más objektumoknak, például a IMessageWriter szolgáltatásnak. A szolgáltatás nem kapcsolódik webszolgáltatáshoz, bár lehet, hogy webszolgáltatást használ.

Tegyük fel például, hogy az IMessageWriter interfész határozza meg a metódust Write . Ezt az interfészt egy konkrét típus implementálja, MessageWriteramely korábban is látható. Az alábbi mintakód regisztrálja a szolgáltatást a IMessageWriter konkrét típussal MessageWriter. A AddSingleton metódus egyszeri élettartammal regisztrálja a szolgáltatást, ami azt jelenti, hogy az alkalmazás leállítása után nem lesz megsemmisítve.

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>

Az előző kód példában a kiemelt sorok:

  • Hozzon létre egy gazdagép-alkalmazás készítő példányt.
  • Konfigurálja a szolgáltatásokat úgy, hogy regisztrálja az Workerüzemeltetett szolgáltatást és az IMessageWriter interfészt önálló szolgáltatásként az MessageWriter osztály megfelelő implementációjával.
  • Hozza létre és futtassa a gazdagépet.

A gazdagép tartalmazza a függőséginjektálási szolgáltatót. Emellett tartalmazza az összes többi kapcsolódó szolgáltatást is, amely az automatikus példányosításhoz és a Worker megfelelő IMessageWriter implementáció argumentumként való biztosításához szükséges.

A DI-minta használatával a feldolgozó szolgáltatás nem a konkrét típust MessageWriterhasználja, csak azt a IMessageWriter felületet, amelyet megvalósít. Ez a kialakítás megkönnyíti a munkavégző szolgáltatás által használt implementáció módosítását a feldolgozó szolgáltatás módosítása nélkül. A feldolgozó szolgáltatás szintén nem hoz létre példányt.MessageWriter A DI-tároló létrehozza a példányt.

Tegyük fel, hogy a MessageWriter egy olyan típusra szeretne váltani, amely a keretrendszer által biztosított naplózási szolgáltatást használja. Hozzon létre egy olyan osztályt LoggingMessageWriter , amely a ILogger<TCategoryName> konstruktorban való kéréssel függ.

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

A MessageWriter-ról a LoggingMessageWriter-ra való váltáshoz egyszerűen frissítse a AddSingleton hívást az új IMessageWriter implementáció regisztrálásához.

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

Jótanács

A tároló ILogger<TCategoryName> felhasználásával feloldja , ami szükségtelenné teszi az összes (általános) létrehozott típus regisztrálását.

Konstruktorinjektálási viselkedés

A szolgáltatások feloldhatók a IServiceProvider (a beépített szolgáltatástároló) vagy ActivatorUtilities segítségével. ActivatorUtilities olyan objektumokat hoz létre, amelyek nincsenek regisztrálva a tárolóban, és amelyek bizonyos keretrendszerfunkciókkal vannak használatban.

A konstruktorok elfogadhatják a függőséginjektálás által nem biztosított argumentumokat, de az argumentumoknak alapértelmezett értékeket kell hozzárendelniük.

Amikor IServiceProvider vagy ActivatorUtilities szolgáltatásokat old fel, a konstruktorinjektáláshoz nyilvános konstruktor szükséges.

Szolgáltatások feloldásakor a ActivatorUtilities esetében a konstruktorinjektálás azt követeli meg, hogy csak egyetlen alkalmazható konstruktor létezzen. A konstruktor túlterhelések támogatottak, de csak egy túlterhelés létezhet, amelynek argumentumai függőséginjektálással mind teljesíthetők.

Konstruktorválasztási szabályok

Ha egy típus több konstruktort határoz meg, a szolgáltató logikája határozza meg, hogy melyik konstruktort használja. A legtöbb paraméterrel rendelkező konstruktor, ahol a típusok DI-feloldhatóak, ki van választva. Tekintse meg a következő példaszolgáltatást:

public class ExampleService
{
    public ExampleService()
    {
    }

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

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

Az előző kódban feltételezzük, hogy a naplózás hozzá lett adva, és elérhető a szolgáltatótól, de a ServiceA és ServiceB típusok nem. A ILogger<ExampleService> paraméter konstruktor feloldja a ExampleService példányt. Annak ellenére, hogy van egy konstruktor, amely több paramétert határoz meg, a ServiceA és ServiceB típusok nem oldhatók fel függőségi injekcióval.

Ha a konstruktorok felderítésekor kétértelműség áll fenn, a rendszer kivételt jelez. Tekintse meg a következő C#-példaszolgáltatást:

Figyelmeztetés

Ez a ExampleService nem egyértelmű DI-feloldható típusparaméterekkel rendelkező kód kivételt jelez. Ne tegye ezt – a célja, hogy bemutassa, mit jelent a "nem egyértelmű DI-feloldható típusok" kifejezés.

public class ExampleService
{
    public ExampleService()
    {
    }

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

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

Az előző példában három konstruktor található. Az első konstruktor paraméter nélküli, és nem igényel szolgáltatást a szolgáltatótól. Tegyük fel, hogy a naplózás és a beállítások is hozzáadva lettek a DI-tárolóhoz, és di-feloldható szolgáltatások. Amikor a DI-tároló megpróbálja feloldani a ExampleService típust, kivételt jelez, mivel a két konstruktor nem egyértelmű.

Kerülje a kétértelműséget egy olyan konstruktor definiálásával, amely mindkét DI-feloldható típust elfogadja:

public class ExampleService
{
    public ExampleService()
    {
    }

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

Hatókör érvényesítése

A hatókörön belüli szolgáltatásokat az őket létrehozó tároló kezel le. Ha egy hatókörrel rendelkező szolgáltatás jön létre a gyökértárolóban, a szolgáltatás élettartama hatékonyan singleton-ná válik, mert azt csak a gyökértároló szünteti meg, amikor az alkalmazás leáll. A szolgáltatási hatókörök érvényesítése ezeket a helyzeteket akkor észleli, amikor a BuildServiceProvider meg van hívva.

Amikor egy alkalmazás a fejlesztői környezetben fut, és meghívja a CreateApplicationBuildert a gazdagép létrehozásához, az alapértelmezett szolgáltató ellenőrzi, hogy:

  • A hatókörön belüli szolgáltatások nem oldódnak fel a gyökérszolgáltatás-szolgáltatótól.
  • A hatókörön belüli szolgáltatások nem injektálhatók egyetlen dobozba.

Hatókör-forgatókönyvek

A IServiceScopeFactory rendszer mindig egyszeriként van regisztrálva, de ez az IServiceProvider adott osztály élettartamától függően változhat. Ha például egy hatókörből oldja fel a szolgáltatásokat, és ezek bármelyike használ egy IServiceProvider, akkor az egy hatókörön belüli példány.

Ha hatókörkezelési szolgáltatásokat szeretne elérni az olyan implementációkon IHostedServicebelül, mint például az BackgroundService, ne injektálja a szolgáltatásfüggőségeket konstruktorinjektálással. Ehelyett injektáljon IServiceScopeFactory, hozzon létre egy hatókört, majd oldja fel a hatókör függőségeit a megfelelő élettartam használatához.

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

Az előző kódban, miközben az alkalmazás fut, a háttérszolgáltatás:

  • Attól függ, hogy a IServiceScopeFactory.
  • Létrehoz egy más IServiceScope szolgáltatások feloldására szolgáló megoldást.
  • A hatókörön belüli szolgáltatások felhasználási célú feloldása.
  • Dolgozik az objektumok feldolgozásán, majd átfektetésén, és végül megjelöli őket feldolgozottként.

A minta forráskódjából megtudhatja IHostedService , hogy az implementációk hogyan használhatják ki a hatókörön belüli szolgáltatási élettartamokat.

Kulcsos szolgáltatások

A szolgáltatásokat regisztrálhatja, és kulcs alapján végezhet kereséseket. Más szóval több szolgáltatást is regisztrálhat különböző kulcsokkal, és ezt a kulcsot használhatja a kereséshez.

Vegyük például azt az esetet, amikor a felület IMessageWriterkülönböző implementációi vannak: MemoryMessageWriter és QueueMessageWriter.

Ezeket a szolgáltatásokat a (korábban látott) szolgáltatásregisztrációs módszerek túlterhelésével regisztrálhatja, amelyek paraméterként támogatják a kulcsot:

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

Ez key nem korlátozódik a string. A key bármilyen object lehet, amennyiben a típus megfelelően implementálja Equals.

Az osztály IMessageWriterkonstruktorában adja hozzá a FromKeyedServicesAttribute feloldani kívánt szolgáltatás kulcsát:

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

KeyedService.AnyKey tulajdonság

A KeyedService.AnyKey tulajdonság egy speciális kulcsot biztosít a kulcsos szolgáltatásokhoz. Regisztrálhat egy szolgáltatást úgy, hogy KeyedService.AnyKey tartalékként használjuk, amely minden kulcsot lefed. Ez akkor hasznos, ha egy olyan kulcs alapértelmezett implementációját szeretné megadni, amely nem rendelkezik explicit regisztrációval.

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

Az előző példában:

  • A ICache kérése a "premium" kulccsal visszaadja a PremiumCache példányt.
  • Bármely más kulccsal (például ICache vagy "basic") való "standard" kérés egy új DefaultCache-t hoz létre a AnyKey visszaesési lehetőség használatával.

Fontos

.NET 10-től kezdve a GetKeyedService() a KeyedService.AnyKey hívásakor kivált egy InvalidOperationException-t, mivel a AnyKey regisztrációs tartalékként van szánva, nem pedig lekérdezési kulcsként. További információkért lásd: A GetKeyedService() és a GetKeyedServices() problémáinak megoldása az AnyKey használatával.

Lásd még