.NET-beroendeinmatning

.NET stöder designmönstret för beroendeinmatning (DI), vilket är en teknik för att uppnå inversion av kontroll (IoC) mellan klasser och deras beroenden. Beroendeinmatning i .NET är en inbyggd del av ramverket, tillsammans med konfiguration, loggning och alternativmönstret.

Ett beroende är ett objekt som ett annat objekt är beroende av. Granska följande MessageWriter klass med en Write metod som andra klasser är beroende av:

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

En klass kan skapa en instans av MessageWriter klassen för att använda sin Write metod. I följande exempel MessageWriter är klassen ett beroende av Worker klassen:

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

Klassen skapar och är direkt beroende av MessageWriter klassen. Hårdkodade beroenden, till exempel i föregående exempel, är problematiska och bör undvikas av följande skäl:

  • Om du vill ersätta MessageWriter med en annan implementering Worker måste klassen ändras.
  • Om MessageWriter det finns beroenden måste de också konfigureras av Worker klassen. I ett stort projekt med flera klasser beroende på MessageWriterblir konfigurationskoden utspridd över appen.
  • Den här implementeringen är svår att enhetstesta. Appen bör använda en modell- eller stub-klass MessageWriter , vilket inte är möjligt med den här metoden.

Beroendeinmatning åtgärdar dessa problem genom:

  • Användning av ett gränssnitt eller en basklass för att abstrahera beroendeimplementeringen.
  • Registrering av beroendet i en tjänstcontainer. .NET tillhandahåller en inbyggd tjänstcontainer, IServiceProvider. Tjänsterna registreras vanligtvis vid appens start och läggs till i en IServiceCollection. När alla tjänster har lagts till använder BuildServiceProvider du för att skapa tjänstcontainern.
  • Inmatning av tjänsten i konstruktorn för den klass där den används. Ramverket tar på sig ansvaret att skapa en instans av beroendet och ta bort det när det inte längre behövs.

Gränssnittet definierar Write till exempel IMessageWriter metoden:

namespace DependencyInjection.Example;

public interface IMessageWriter
{
    void Write(string message);
}

Det här gränssnittet implementeras av en konkret typ, MessageWriter:

namespace DependencyInjection.Example;

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

Exempelkoden registrerar IMessageWriter tjänsten med betongtypen MessageWriter. Metoden AddSingleton registrerar tjänsten med en singleton-livslängd, appens livslängd. Tjänstens livslängd beskrivs senare i den här artikeln.

using DependencyInjection.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

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

using IHost host = builder.Build();

host.Run();

I föregående kod, exempelappen:

  • Skapar en värdappsbyggareinstans.

  • Konfigurerar tjänsterna genom att registrera:

    • Som Worker en värdbaserad tjänst. Mer information finns i Worker Services i .NET.
    • Gränssnittet IMessageWriter som en singleton-tjänst med en motsvarande implementering av MessageWriter klassen.
  • Skapar värden och kör den.

Värden innehåller providern för beroendeinmatningstjänsten. Den innehåller också alla andra relevanta tjänster som krävs för att automatiskt instansiera Worker och tillhandahålla motsvarande IMessageWriter implementering som ett argument.

namespace DependencyInjection.Example;

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

Med hjälp av DI-mönstret:

  • Använder inte betongtypen MessageWriter, bara gränssnittet IMessageWriter som implementerar den. Det gör det enkelt att ändra den implementering som arbetartjänsten använder utan att ändra arbetstjänsten.
  • Skapar inte en instans av MessageWriter. Instansen skapas av DI-containern.

Implementeringen av IMessageWriter gränssnittet kan förbättras med hjälp av det inbyggda loggnings-API:et:

namespace DependencyInjection.Example;

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

Den uppdaterade AddSingleton metoden registrerar den nya IMessageWriter implementeringen:

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

Typen HostApplicationBuilder (builder) är en del av NuGet-paketet Microsoft.Extensions.Hosting .

LoggingMessageWriter beror på ILogger<TCategoryName>, som den begär i konstruktorn. ILogger<TCategoryName> är en ramverksbaserad tjänst.

Det är inte ovanligt att använda beroendeinmatning på ett kedjat sätt. Varje begärt beroende begär i sin tur sina egna beroenden. Containern löser beroendena i diagrammet och returnerar den fullständigt lösta tjänsten. Den kollektiva uppsättningen beroenden som måste lösas kallas vanligtvis ett beroendeträd, beroendediagram eller objektdiagram.

Containern löser ILogger<TCategoryName> problemet genom att dra nytta av (generiska) öppna typer, vilket eliminerar behovet av att registrera alla (generiska) konstruerade typer.

Med terminologi för beroendeinmatning, en tjänst:

  • Är vanligtvis ett objekt som tillhandahåller en tjänst till andra objekt, till exempel tjänsten IMessageWriter .
  • Är inte relaterad till en webbtjänst, även om tjänsten kan använda en webbtjänst.

Ramverket tillhandahåller ett robust loggningssystem. Implementeringarna IMessageWriter som visas i föregående exempel skrevs för att demonstrera grundläggande DI, inte för att implementera loggning. De flesta appar ska inte behöva skriva loggare. Följande kod visar hur du använder standardloggningen, som endast kräver att den Worker registreras som en värdbaserad tjänst AddHostedService:

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;

    public Worker(ILogger<Worker> logger) =>
        _logger = logger;

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

Med hjälp av föregående kod behöver du inte uppdatera Program.cs eftersom loggning tillhandahålls av ramverket.

Flera identifieringsregler för konstruktor

När en typ definierar mer än en konstruktor har tjänstleverantören logik för att avgöra vilken konstruktor som ska användas. Konstruktorn med de flesta parametrar där typerna är DI-matchbara väljs. Överväg följande C#-exempeltjänst:

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(ILogger<ExampleService> logger)
    {
        // omitted for brevity
    }

    public ExampleService(FooService fooService, BarService barService)
    {
        // omitted for brevity
    }
}

I föregående kod förutsätter du att loggning har lagts till och kan matchas från tjänstleverantören, men det är inte typerna FooService och BarService . Konstruktorn med parametern ILogger<ExampleService> används för att lösa instansen ExampleService . Även om det finns en konstruktor som definierar fler parametrar är typerna FooService och BarService inte DI-matchbara.

Om det finns tvetydigheter vid identifiering av konstruktorer utlöses ett undantag. Överväg följande C#-exempeltjänst:

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(ILogger<ExampleService> logger)
    {
        // omitted for brevity
    }

    public ExampleService(IOptions<ExampleOptions> options)
    {
        // omitted for brevity
    }
}

Varning

Koden ExampleService med tvetydiga DI-matchningsbara typparametrar skulle utlösa ett undantag. Gör inte detta – det är avsett att visa vad som menas med "tvetydiga DI-matchningsbara typer".

I föregående exempel finns det tre konstruktorer. Den första konstruktorn är parameterlös och kräver inga tjänster från tjänstleverantören. Anta att både loggning och alternativ har lagts till i DI-containern och är DI-matchningsbara tjänster. När DI-containern försöker matcha ExampleService typen utlöser den ett undantag eftersom de två konstruktorerna är tvetydiga.

Du kan undvika tvetydighet genom att definiera en konstruktor som accepterar båda DI-matchningsbara typerna i stället:

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(
        ILogger<ExampleService> logger,
        IOptions<ExampleOptions> options)
    {
        // omitted for brevity
    }
}

Registrera grupper av tjänster med tilläggsmetoder

Microsoft Extensions använder en konvention för att registrera en grupp relaterade tjänster. Konventionen är att använda en enda Add{GROUP_NAME} tilläggsmetod för att registrera alla tjänster som krävs av en ramverksfunktion. Till exempel AddOptions registrerar tilläggsmetoden alla tjänster som krävs för att använda alternativ.

Ramverksbaserade tjänster

När du använder något av de tillgängliga värd- eller appbyggarmönstren tillämpas standardvärden och tjänster registreras av ramverket. Överväg några av de mest populära värd- och appbyggarmönstren:

När du har skapat en byggare från någon av dessa API:er IServiceCollection har tjänsterna definierats av ramverket, beroende på hur värden har konfigurerats. För appar baserade på .NET-mallar kan ramverket registrera hundratals tjänster.

I följande tabell visas ett litet urval av dessa ramverksregistrerade tjänster:

Tjänsttyp Livstid
Microsoft.Extensions.DependencyInjection.IServiceScopeFactory Singleton
IHostApplicationLifetime Singleton
Microsoft.Extensions.Logging.ILogger<TCategoryName> Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Tillfälligt
Microsoft.Extensions.Options.IOptions<TOptions> Singleton
System.Diagnostics.DiagnosticListener Singleton
System.Diagnostics.DiagnosticSource Singleton

Tjänstlivslängd

Tjänster kan registreras med någon av följande livslängder:

I följande avsnitt beskrivs var och en av de föregående livslängderna. Välj en lämplig livslängd för varje registrerad tjänst.

Tillfälligt

Tillfälliga livslängdstjänster skapas varje gång de begärs från tjänstcontainern. Om du vill registrera en tjänst som tillfällig anropar du AddTransient.

I appar som bearbetar begäranden tas tillfälliga tjänster bort i slutet av begäran. Den här livslängden medför allokeringar per/begäran, eftersom tjänster löses och konstrueras varje gång. Mer information finns i Riktlinjer för beroendeinmatning: IDisposable-vägledning för tillfälliga och delade instanser.

Scoped

För webbprogram anger en begränsad livslängd att tjänster skapas en gång per klientbegäran (anslutning). Registrera begränsade tjänster med AddScoped.

I appar som bearbetar begäranden tas begränsade tjänster bort i slutet av begäran.

När du använder Entity Framework Core AddDbContext registrerar DbContext tilläggsmetoden typer med en begränsad livslängd som standard.

Kommentar

Lös inte en begränsad tjänst från en singleton och var noga med att inte göra det indirekt, till exempel via en tillfällig tjänst. Det kan orsaka att tjänsten har felaktigt tillstånd när efterföljande begäranden bearbetas. Det är okej att:

  • Lös en singleton-tjänst från en begränsad eller tillfällig tjänst.
  • Lös en begränsad tjänst från en annan begränsad eller tillfällig tjänst.

I utvecklingsmiljön genererar som standard en lösning av en tjänst från en annan tjänst med längre livslängd ett undantag. Mer information finns i Omfångsverifiering.

Singleton

Singleton Lifetime-tjänster skapas antingen:

  • Första gången de begärs.
  • Av utvecklaren när du tillhandahåller en implementeringsinstans direkt till containern. Den här metoden behövs sällan.

Varje efterföljande begäran om tjänstimplementeringen från containern för beroendeinmatning använder samma instans. Om appen kräver singleton-beteende tillåter du att tjänstcontainern hanterar tjänstens livslängd. Implementera inte designmönstret för singleton och ange kod för att ta bort singletonen. Tjänster ska aldrig tas bort med kod som löste tjänsten från containern. Om en typ eller fabrik registreras som en singleton tas singletonen bort automatiskt av containern.

Registrera singleton-tjänster med AddSingleton. Singleton-tjänster måste vara trådsäkra och används ofta i tillståndslösa tjänster.

I appar som bearbetar begäranden tas singleton-tjänster bort när de ServiceProvider tas bort vid programavstängning. Eftersom minnet inte släpps förrän appen har stängts av bör du överväga minnesanvändning med en singleton-tjänst.

Metoder för tjänstregistrering

Ramverket tillhandahåller metoder för tjänstregistreringstillägg som är användbara i specifika scenarier:

Metod Automatisk
objekt
Förfogande
Flera
Implementeringar
Passera args
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>()

Exempel:

services.AddSingleton<IMyDep, MyDep>();
Ja Ja Nej
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION})

Exempel:

services.AddSingleton<IMyDep>(sp => new MyDep());
services.AddSingleton<IMyDep>(sp => new MyDep(99));
Ja Ja Ja
Add{LIFETIME}<{IMPLEMENTATION}>()

Exempel:

services.AddSingleton<MyDep>();
Ja No Nej
AddSingleton<{SERVICE}>(new {IMPLEMENTATION})

Exempel:

services.AddSingleton<IMyDep>(new MyDep());
services.AddSingleton<IMyDep>(new MyDep(99));
Nej Ja Ja
AddSingleton(new {IMPLEMENTATION})

Exempel:

services.AddSingleton(new MyDep());
services.AddSingleton(new MyDep(99));
Nej No Ja

Mer information om typhantering finns i avsnittet Avyttring av tjänster .

Registrering av en tjänst med endast en implementeringstyp motsvarar registrering av tjänsten med samma implementerings- och tjänsttyp. Det är därför flera implementeringar av en tjänst inte kan registreras med de metoder som inte använder en explicit tjänsttyp. Dessa metoder kan registrera flera instanser av en tjänst, men alla har samma implementeringstyp .

Någon av ovanstående tjänstregistreringsmetoder kan användas för att registrera flera tjänstinstanser av samma tjänsttyp. I följande exempel AddSingleton anropas två gånger med IMessageWriter som tjänsttyp. Det andra anropet till AddSingleton åsidosätter det föregående när det matchas som IMessageWriter och läggs till i föregående när flera tjänster matchas via IEnumerable<IMessageWriter>. Tjänsterna visas i den ordning de registrerades när de löstes via IEnumerable<{SERVICE}>.

using ConsoleDI.IEnumerableExample;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

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

using IHost host = builder.Build();

_ = host.Services.GetService<ExampleService>();

await host.RunAsync();

Föregående exempelkälla registrerar två implementeringar av IMessageWriter.

using System.Diagnostics;

namespace ConsoleDI.IEnumerableExample;

public sealed class ExampleService
{
    public ExampleService(
        IMessageWriter messageWriter,
        IEnumerable<IMessageWriter> messageWriters)
    {
        Trace.Assert(messageWriter is LoggingMessageWriter);

        var dependencyArray = messageWriters.ToArray();
        Trace.Assert(dependencyArray[0] is ConsoleMessageWriter);
        Trace.Assert(dependencyArray[1] is LoggingMessageWriter);
    }
}

Definierar ExampleService två konstruktorparametrar: en enda IMessageWriter, och en IEnumerable<IMessageWriter>. Den enda IMessageWriter är den sista implementeringen som har registrerats, medan den IEnumerable<IMessageWriter> representerar alla registrerade implementeringar.

Ramverket innehåller TryAdd{LIFETIME} också tilläggsmetoder som endast registrerar tjänsten om det inte redan finns en registrerad implementering.

I följande exempel registreras anropet till AddSingleton som en implementering för IMessageWriter.ConsoleMessageWriter Anropet till TryAddSingleton har ingen effekt eftersom IMessageWriter det redan har en registrerad implementering:

services.AddSingleton<IMessageWriter, ConsoleMessageWriter>();
services.TryAddSingleton<IMessageWriter, LoggingMessageWriter>();

Har TryAddSingleton ingen effekt, eftersom det redan har lagts till och "try" misslyckas. Skulle ExampleService hävda följande:

public class ExampleService
{
    public ExampleService(
        IMessageWriter messageWriter,
        IEnumerable<IMessageWriter> messageWriters)
    {
        Trace.Assert(messageWriter is ConsoleMessageWriter);
        Trace.Assert(messageWriters.Single() is ConsoleMessageWriter);
    }
}

Mer information finns i:

Metoderna TryAddEnumerable (ServiceDescriptor) registrerar endast tjänsten om det inte redan finns en implementering av samma typ. Flera tjänster löses via IEnumerable<{SERVICE}>. När du registrerar tjänster lägger du till en instans om en av samma typer inte redan har lagts till. Biblioteksförfattare använder TryAddEnumerable för att undvika att registrera flera kopior av en implementering i containern.

I följande exempel registreras MessageWriter det första anropet till TryAddEnumerable som en implementering för IMessageWriter1. Det andra anropet registreras MessageWriter för IMessageWriter2. Det tredje anropet har ingen effekt eftersom IMessageWriter1 det redan har en registrerad implementering av MessageWriter:

public interface IMessageWriter1 { }
public interface IMessageWriter2 { }

public class MessageWriter : IMessageWriter1, IMessageWriter2
{
}

services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter1, MessageWriter>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter2, MessageWriter>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter1, MessageWriter>());

Tjänstregistrering är vanligtvis orderoberoende, förutom när du registrerar flera implementeringar av samma typ.

IServiceCollection är en samling ServiceDescriptor objekt. I följande exempel visas hur du registrerar en tjänst genom att skapa och lägga till en ServiceDescriptor:

string secretKey = Configuration["SecretKey"];
var descriptor = new ServiceDescriptor(
    typeof(IMessageWriter),
    _ => new DefaultMessageWriter(secretKey),
    ServiceLifetime.Transient);

services.Add(descriptor);

De inbyggda Add{LIFETIME} metoderna använder samma metod. Se till exempel AddScoped-källkoden.

Konstruktorinmatningsbeteende

Tjänster kan lösas med hjälp av:

Konstruktorer kan acceptera argument som inte tillhandahålls av beroendeinmatning, men argumenten måste tilldela standardvärden.

När tjänster löses av IServiceProvider eller ActivatorUtilitieskräver konstruktorinmatning en offentlig konstruktor.

När tjänster löses av ActivatorUtilitieskräver konstruktorinmatning att det bara finns en tillämplig konstruktor. Konstruktoröverlagring stöds, men det finns bara en överlagring vars argument kan uppfyllas genom beroendeinmatning.

Omfångsverifiering

När appen körs i Development miljön och anropar CreateApplicationBuilder för att skapa värden utför standardtjänstleverantören kontroller för att kontrollera att:

  • Begränsade tjänster matchas inte från rottjänstleverantören.
  • Begränsade tjänster matas inte in i singletons.

Rottjänstprovidern skapas när BuildServiceProvider anropas. Rottjänstleverantörens livslängd motsvarar appens livslängd när providern börjar med appen och tas bort när appen stängs av.

Begränsade tjänster tas bort av containern som skapade dem. Om en begränsad tjänst skapas i rotcontainern höjs tjänstens livslängd effektivt till singleton eftersom den endast tas bort av rotcontainern när appen stängs av. Validering av tjänstomfattningar fångar upp dessa situationer när BuildServiceProvider anropas.

Omfångsscenarier

IServiceScopeFactory är alltid registrerad som en singleton, men IServiceProvider kan variera beroende på livslängden för den innehållande klassen. Om du till exempel löser tjänster från ett omfång och någon av dessa tjänster tar en IServiceProvider, blir det en begränsad instans.

För att uppnå omfångstjänster inom implementeringar av IHostedService, till exempel BackgroundService, ska du inte mata in tjänstberoenden via konstruktorinmatning. Mata i stället in IServiceScopeFactory, skapa ett omfång och lös sedan beroenden från omfånget för att använda lämplig tjänstlivslängd.

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

I föregående kod, medan appen körs, är bakgrundstjänsten:

  • Beror på IServiceScopeFactory.
  • Skapar en IServiceScope för att lösa ytterligare tjänster.
  • Löser begränsade tjänster för förbrukning.
  • Fungerar med att bearbeta objekt och sedan vidarebefordra dem och markerar dem slutligen som bearbetade.

I källkodsexemplet kan du se hur implementeringar av kan dra nytta av IHostedService begränsade tjänstlivslängder.

Nyckelade tjänster

Från och med .NET 8 finns det stöd för tjänstregistreringar och sökningar baserat på en nyckel, vilket innebär att det är möjligt att registrera flera tjänster med en annan nyckel och använda den här nyckeln för sökningen.

Tänk till exempel på det fall där du har olika implementeringar av gränssnittet IMessageWriter: MemoryMessageWriter och QueueMessageWriter.

Du kan registrera dessa tjänster med hjälp av överbelastningen av tjänstregistreringsmetoderna (som vi såg tidigare) som stöder en nyckel som en parameter:

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

key är inte begränsat till string, det kan vara vad object du vill, så länge typen implementerar Equalskorrekt .

I konstruktorn för klassen som använder IMessageWriterlägger du till FromKeyedServicesAttribute för att ange nyckeln för tjänsten som ska matchas:

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

Se även