Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
.NET supporta il modello di progettazione software DI ( Dependency Injection ), una tecnica per ottenere l'inversione del controllo (IoC) tra le classi e le relative dipendenze. L'inserimento delle dipendenze in .NET è una parte predefinita del framework, insieme alla configurazione, alla registrazione e al modello di opzioni.
Una dipendenza è un oggetto da cui dipende un altro oggetto. La classe seguente MessageWriter dispone di un Write metodo da cui possono dipendere altre classi:
public class MessageWriter : IMessageWriter
{
public void Write(string message)
{
Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
}
}
Una classe può creare un'istanza della MessageWriter classe per usare il relativo Write metodo. Nell'esempio seguente la MessageWriter classe è una dipendenza della Worker classe :
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);
}
}
}
In questo caso, la Worker classe crea e dipende direttamente dalla MessageWriter classe . Le dipendenze hard-coded come questa sono problematiche e devono essere evitate per i motivi seguenti:
- Per sostituire
MessageWritercon un'implementazione diversa, è necessario modificare laWorkerclasse . - Se
MessageWriterpresenta dipendenze, laWorkerclasse deve anche configurarle. In un progetto di grandi dimensioni con più classi che dipendono daMessageWriter, il codice di configurazione diventa sparso in tutta l'app. - È difficile eseguire unit test di questa implementazione. L'app dovrebbe usare una classe
MessageWriterfittizia o stub, ma ciò non è possibile con questo approccio.
Il concetto
L'inserimento delle dipendenze serve a risolvere i problemi delle dipendenze hard-coded tramite:
L'uso di un'interfaccia o di una classe di base per astrarre l'implementazione delle dipendenze.
Registrazione della dipendenza in un contenitore del servizio.
ASP.NET Core offre il contenitore di servizi predefinito IServiceProvider. I servizi vengono in genere registrati all'avvio dell'app e aggiunti a un oggetto IServiceCollection. Dopo aver aggiunto tutti i servizi, usare BuildServiceProvider per creare il contenitore del servizio.
Inserimento del servizio nel costruttore della classe in cui viene usato.
Il framework si assume la responsabilità della creazione di un'istanza della dipendenza e della sua eliminazione quando non è più necessaria.
Suggerimento
Nella terminologia di inserimento delle dipendenze, un servizio è in genere un oggetto che fornisce un servizio ad altri oggetti, ad esempio il IMessageWriter servizio. Il servizio non è correlato a un servizio Web, anche se potrebbe usare un servizio Web.
Si supponga, ad esempio, che l'interfaccia IMessageWriter definisca il Write metodo . Questa interfaccia viene implementata da un tipo concreto, MessageWriter, illustrato in precedenza. Il codice di esempio seguente registra il servizio IMessageWriter con il tipo concreto MessageWriter. Il AddSingleton metodo registra il servizio con un singleton ciclo di vita, il che significa che non viene eliminato fino all'arresto dell'app.
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>
Nell'esempio di codice precedente le righe evidenziate:
- Crea un'istanza del generatore di app host.
- Configurare i servizi registrando
Workercome servizio ospitato e l'interfacciaIMessageWritercome servizio singleton con un'implementazione corrispondente dellaMessageWriterclasse . - Compila l'host ed eseguilo.
L'host contiene il provider di servizi di inserimento delle dipendenze. Contiene anche tutti gli altri servizi pertinenti necessari per creare automaticamente un'istanza Worker e fornire l'implementazione di IMessageWriter corrispondente come argomento.
Usando il modello di inserimento delle dipendenze, il servizio di lavoro non usa il tipo MessageWriterconcreto , ma solo l'interfaccia IMessageWriter implementata. Questa progettazione semplifica la modifica dell'implementazione usata dal servizio di lavoro senza modificare il servizio di lavoro. Il servizio di lavoro non crea neanche un'istanza di MessageWriter. Il contenitore DI crea l'istanza.
Immagina di voler sostituire MessageWriter con un tipo che utilizza il servizio di registrazione fornito dal framework. Creare una classe LoggingMessageWriter che dipende da ILogger<TCategoryName>, richiedendola nel costruttore.
public class LoggingMessageWriter(
ILogger<LoggingMessageWriter> logger) : IMessageWriter
{
public void Write(string message) =>
logger.LogInformation("Info: {Msg}", message);
}
Per passare da MessageWriter a LoggingMessageWriter, aggiornare semplicemente la chiamata a AddSingleton per registrare questa nuova IMessageWriter implementazione:
builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>();
Suggerimento
Il contenitore viene ILogger<TCategoryName> risolto sfruttando i tipi aperti (generici), eliminando la necessità di registrare ogni tipo costruito (generico).
Comportamento dell'inserimento del costruttore
I servizi possono essere risolti usando IServiceProvider (il contenitore del servizio predefinito) o ActivatorUtilities.
ActivatorUtilities crea oggetti non registrati nel contenitore e viene usato con alcune funzionalità del framework.
I costruttori possono accettare argomenti non forniti tramite l'inserimento di dipendenze, ma gli argomenti devono assegnare valori predefiniti.
Quando i servizi IServiceProvider o ActivatorUtilities vengono risolti, l'inserimento nel costruttore richiede un costruttore pubblico.
Quando ActivatorUtilities risolve i servizi, l'iniezione del costruttore richiede l'esistenza di un solo costruttore appropriato. Sebbene siano supportati gli overload dei costruttori, può essere presente un solo overload i cui argomenti possono essere tutti specificati tramite l'inserimento delle dipendenze.
Regole di selezione del costruttore
Quando un tipo definisce più costruttori, il provider di servizi ha la logica per determinare quale costruttore usare. Viene selezionato il costruttore con la maggior parte dei parametri in cui i tipi sono risolvibili dall'inserimento delle dipendenze. Si consideri il servizio di esempio seguente:
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(ILogger<ExampleService> logger)
{
// ...
}
public ExampleService(ServiceA serviceA, ServiceB serviceB)
{
// ...
}
}
Nel codice precedente, supponi che il logging sia stato aggiunto e sia risolvibile dal provider di servizi, ma i tipi ServiceA e ServiceB non lo siano. Il costruttore con il ILogger<ExampleService> parametro risolve l'istanza ExampleService . Anche se è presente un costruttore che definisce più parametri, i tipi ServiceA e ServiceB non sono risolvibili da DI.
Se si verificano ambiguità durante l'individuazione dei costruttori, viene generata un'eccezione. Si consideri il servizio di esempio C# seguente:
Avvertimento
Questo ExampleService codice con parametri di tipo ambigui risolvibili tramite Dependency Injection genera un'eccezione.
Non farlo: è destinato a mostrare cosa significa "tipi DI risolvibili ambigui".
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(ILogger<ExampleService> logger)
{
// ...
}
public ExampleService(IOptions<ExampleOptions> options)
{
// ...
}
}
Nell'esempio precedente sono presenti tre costruttori. Il primo costruttore è senza parametri e non richiede alcun servizio del provider di servizi. Si supponga che sia la registrazione che le opzioni siano state aggiunte al contenitore di inserimento delle dipendenze e siano servizi risolvibili con l'inserimento delle dipendenze. Quando il contenitore di inversione delle dipendenze tenta di risolvere il tipo ExampleService, genera un'eccezione, poiché i due costruttori sono ambigui.
Evitare ambiguità definendo invece un costruttore che accetta entrambi i tipi risolvibili dall'inserimento delle dipendenze:
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(
ILogger<ExampleService> logger,
IOptions<ExampleOptions> options)
{
// ...
}
}
Convalida dell'ambito
I servizi a scopo vengono gestiti dal contenitore che li ha creati. Se un servizio con ambito viene creato nel contenitore radice, la durata del servizio viene promossa in modo efficace al singleton perché viene eliminata solo dal contenitore radice quando l'app viene arrestata. La convalida degli ambiti servizio rileva queste situazioni nel contesto della chiamata di BuildServiceProvider.
Quando un'app viene eseguita nell'ambiente di sviluppo e chiama CreateApplicationBuilder per compilare l'host, il provider di servizi predefinito esegue controlli per verificare che:
- I servizi con ambito non vengano risolti dal provider di servizi radice.
- I servizi con ambito non vengano inseriti in singleton.
Scenari di ambito
IServiceScopeFactory viene sempre registrato come singleton, ma IServiceProvider può variare in base alla durata della classe contenitore. Ad esempio, se si risolvono i servizi da un ambito e uno di questi servizi accetta un'istanza IServiceProvidercon ambito , si tratta di un'istanza con ambito.
Per ottenere servizi di ambito all'interno di implementazioni di IHostedService, ad esempio BackgroundService, non inserire le dipendenze del servizio tramite l'inserimento del costruttore. Inserire, invece, IServiceScopeFactory, creare un ambito, quindi risolvere le dipendenze dall'ambito per usare la durata del servizio appropriata.
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);
}
}
}
}
}
Nel codice precedente, mentre l'app è in esecuzione, il servizio in background:
- Dipende da IServiceScopeFactory.
- Crea un oggetto IServiceScope per la risoluzione di altri servizi.
- Risolve i servizi con ambito per l'utilizzo.
- Funziona sull'elaborazione degli oggetti e quindi sull'inoltro e infine li contrassegna come elaborati.
Dal codice sorgente di esempio è possibile vedere in che modo le implementazioni di IHostedService possono trarre vantaggio dalle durate del servizio con ambito.
Servizi con chiave
È possibile registrare i servizi ed eseguire ricerche in base a una chiave. In altre parole, è possibile registrare più servizi con chiavi diverse e usare questa chiave per la ricerca.
Si consideri ad esempio il caso in cui sono presenti implementazioni diverse dell'interfaccia IMessageWriter: MemoryMessageWriter e QueueMessageWriter.
È possibile registrare questi servizi usando l'overload dei metodi di registrazione del servizio (come illustrato in precedenza) che supporta una chiave come parametro:
services.AddKeyedSingleton<IMessageWriter, MemoryMessageWriter>("memory");
services.AddKeyedSingleton<IMessageWriter, QueueMessageWriter>("queue");
key non è limitato a string. Il key può essere qualsiasi object tu desideri, purché il tipo implementi Equals correttamente.
Nel costruttore della classe che usa IMessageWriter, aggiungere FromKeyedServicesAttribute per specificare la chiave del servizio da risolvere:
public class ExampleService
{
public ExampleService(
[FromKeyedServices("queue")] IMessageWriter writer)
{
// Omitted for brevity...
}
}
Proprietà KeyedService.AnyKey
La KeyedService.AnyKey proprietà fornisce una chiave speciale per l'utilizzo dei servizi con chiave. È possibile registrare un servizio usando KeyedService.AnyKey come fallback corrispondente a qualsiasi chiave. Ciò è utile quando si vuole fornire un'implementazione predefinita per qualsiasi chiave che non dispone di una registrazione esplicita.
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}");
Nell'esempio precedente:
- Richiedere
ICachecon la chiave"premium"restituisce l'istanzaPremiumCache. - Richiedendo
ICachecon qualsiasi altra chiave (ad esempio"basic"o"standard") crea un nuovoDefaultCacheusando il fallbackAnyKey.
Importante
A partire da .NET 10, la chiamata GetKeyedService() con KeyedService.AnyKey genera un InvalidOperationException perché AnyKey è previsto come fallback di registrazione, non come chiave di query. Per altre informazioni, vedere Risolvere i problemi in GetKeyedService() e GetKeyedServices() con AnyKey.
Vedere anche
- Guida introduttiva: Nozioni di base di iniezione delle dipendenze
- Esercitazione: Usare l'iniezione delle dipendenze in .NET
- linee guida per l'iniezione delle dipendenze
- Iniezione di dipendenze in ASP.NET Core
- Modelli di conferenza NDC per lo sviluppo di app di inserimento delle dipendenze
- Principio esplicito delle dipendenze
- L’inversione dei contenitori di controllo e il modello di inserimento delle dipendenze (Martin Fowler)