Not
Åtkomst till denna sida kräver auktorisation. Du kan prova att logga in eller byta katalog.
Åtkomst till denna sida kräver auktorisation. Du kan prova att byta katalog.
.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. Följande MessageWriter klass har en Write metod som andra klasser kan vara beroende av:
public class MessageWriter : IMessageWriter
{
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);
}
}
}
I det här fallet skapar Worker klassen och är direkt beroende av MessageWriter. Hårdkodade beroenden som dessa är problematiska och bör undvikas av följande skäl:
- Om du vill ersätta
MessageWritermed en annan implementering måste du ändraWorkerklassen. - Om
MessageWriterhar beroenden måste klassenWorkerockså konfigurera dem. 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-
MessageWriter-klass, vilket inte är möjligt med den här metoden.
Konceptet
Beroendeinmatning åtgärdar hårdkodade beroendeproblem 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 du BuildServiceProvider för att skapa tjänstcontainern.
Infogning 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.
Tips/Råd
I sambandsinmatningsterminologi är en tjänst vanligtvis ett objekt som tillhandahåller en tjänst till andra objekt, till exempel tjänsten IMessageWriter . Tjänsten är inte relaterad till en webbtjänst, även om den kan använda en webbtjänst.
Anta till exempel att IMessageWriter gränssnittet definierar Write metoden. Det här gränssnittet implementeras av en konkret typ, , MessageWritersom visades tidigare. Följande exempelkod registrerar IMessageWriter tjänsten med betongtypen MessageWriter. Metoden AddSingleton registrerar tjänsten med en singleton-livslängd, vilket innebär att den inte tas bort förrän appen stängs av.
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>
I föregående kodexempel visas de markerade raderna:
- Skapa en värdappsbyggareinstans.
- Konfigurera tjänsterna genom att registrera
Workersom en värdbaserad tjänst ochIMessageWritergränssnittet som en singleton-tjänst med en motsvarande implementering avMessageWriter-klassen. - Skapa värden (hosten) 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.
Genom att använda DI-mönstret använder arbetartjänsten inte betongtypen MessageWriter, utan endast gränssnittet IMessageWriter som den implementerar. Den här designen gör det enkelt att ändra den implementering som arbetartjänsten använder utan att ändra arbetstjänsten. Arbetstjänsten skapar inte heller någon instans av MessageWriter. DI-containern skapar instansen.
Anta nu att du vill växla ut MessageWriter med en typ som använder den ramverksbaserade loggningstjänsten. Skapa en klass LoggingMessageWriter som är ILogger<TCategoryName> beroende av genom att begära den i konstruktorn.
public class LoggingMessageWriter(
ILogger<LoggingMessageWriter> logger) : IMessageWriter
{
public void Write(string message) =>
logger.LogInformation("Info: {Msg}", message);
}
Om du vill växla från MessageWriter till LoggingMessageWriteruppdaterar du helt enkelt anropet till AddSingleton för att registrera den här nya IMessageWriter implementeringen:
builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>();
Tips/Råd
Containern löser ILogger<TCategoryName> problemet genom att dra nytta av (generiska) öppna typer, vilket eliminerar behovet av att registrera alla (generiska) konstruerade typer.
Konstruktorinmatningsbeteende
Tjänster kan lösas med hjälp av IServiceProvider (den inbyggda tjänstcontainern) eller ActivatorUtilities.
ActivatorUtilities skapar objekt som inte är registrerade i containern och som används med vissa ramverksfunktioner.
Konstruktorer kan acceptera argument som inte tillhandahålls av beroendeinmatning, men argumenten måste tilldela standardvärden.
När IServiceProvider eller ActivatorUtilities löser tjänster kräver konstruktorinjektion en offentlig konstruktor.
När ActivatorUtilities hanterar tjänster kräver konstruktorinjektion att det bara finns en tillämplig konstruktor. Konstruktoröverlagring stöds, men det finns bara en överlagring vars argument kan uppfyllas genom beroendeinmatning.
Regler för konstruktorval
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 exempeltjänst:
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(ILogger<ExampleService> logger)
{
// ...
}
public ExampleService(ServiceA serviceA, ServiceB serviceB)
{
// ...
}
}
I föregående kod antar du att loggning har lagts till och kan lösas från tjänstleverantören, men att typerna ServiceA och ServiceB inte kan lösas. Konstruktorn med parametern ILogger<ExampleService> löser instansen ExampleService . Även om det finns en konstruktor som definierar fler parametrar är typerna ServiceA och ServiceB inte DI-matchbara.
Om det finns tvetydigheter vid identifiering av konstruktorer utlöses ett undantag. Överväg följande C#-exempeltjänst:
Varning
Den här ExampleService koden med tvetydiga DI-resolverbara typparametrar kastar ett undantag.
Gör inte detta – det är avsett att visa vad som menas med "tvetydiga DI-lösbara typer".
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(ILogger<ExampleService> logger)
{
// ...
}
public ExampleService(IOptions<ExampleOptions> options)
{
// ...
}
}
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.
Undvik tvetydighet genom att definiera en konstruktor som accepterar båda DI-matchbara typerna i stället:
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(
ILogger<ExampleService> logger,
IOptions<ExampleOptions> options)
{
// ...
}
}
Omfångsverifiering
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änstens omfång identifierar dessa situationer när BuildServiceProvider anropas.
När en app körs i utvecklingsmiljön och anropar CreateApplicationBuilder för att skapa värden för applikationen, kontrollerar standardtjänstleverantören att:
- Begränsade tjänster matchas inte från rottjänstleverantören.
- Begränsade tjänster matas inte in i singletons.
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, är det en begränsad instans.
Om du vill 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 hantera andra 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
Du kan registrera tjänster och utföra sökningar baserat på en nyckel. Med andra ord är det möjligt att registrera flera tjänster med olika nycklar 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.
key kan vara vilken object som helst, så länge typen implementerar Equals korrekt.
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...
}
}
KeyedService.AnyKey-egenskapen
Egenskapen KeyedService.AnyKey tillhandahåller en särskild nyckel för att använda nyckelade tjänster. Du kan registrera en tjänst med KeyedService.AnyKey som reserv som matchar valfri nyckel. Det här är användbart när du vill tillhandahålla en standardimplementering för alla nycklar som inte har någon explicit registrering.
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}");
I föregående exempel:
- Begäran av
ICachemed nyckeln"premium"returnerarPremiumCache-instansen. - Om du begär
ICachemed någon annan nyckel (t.ex"basic". eller"standard") skapas en nyDefaultCachemed hjälp av återställningenAnyKey.
Viktigt!
Från och med .NET 10 genererar anrop GetKeyedService() med KeyedService.AnyKey en InvalidOperationException eftersom AnyKey är avsedd som en reserv för registrering, inte som en frågenyckel. Mer information finns i Åtgärda problem i GetKeyedService() och GetKeyedServices() med AnyKey.
Se även
- Snabbstart: Grunderna för beroendeinmatning
- Självstudie: Använda beroendeinmatning i .NET
- Riktlinjer för beroendeinmatning
- Beroendeinmatning i ASP.NET Core
- NDC-konferensmönster för DI-apputveckling
- Explicit beroendeprincip
- Inversion av kontrollcontainrar och beroendeinmatningsmönstret (Martin Fowler)