Inserimento delle dipendenze .NET

.NET supporta il modello di progettazione software DI (Dependency Injection), una tecnica per ottenere 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. Esaminare la classe MessageWriter seguente con un metodo Write da cui dipendono altre classi:

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

Una classe può creare un'istanza della classe MessageWriter per usare il relativo metodo Write. Nell'esempio seguente la classe MessageWriter è una dipendenza della classe 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);
        }
    }
}

La classe crea e dipende direttamente dalla classe MessageWriter. Le dipendenze hardcoded, ad esempio nell'esempio precedente, sono problematiche e devono essere evitate per i motivi seguenti:

  • Per sostituire MessageWriter con un'implementazione diversa, la classe Worker deve essere modificata.
  • Se MessageWriter presenta delle dipendenze, devono essere configurate anche dalla classe Worker. In un progetto di grandi dimensioni con più classi che dipendono da MessageWriter, il codice di configurazione diventa sparso in tutta l'app.
  • È difficile eseguire unit test di questa implementazione. L'app dovrebbe usare una classe MessageWriter fittizia o stub, ma ciò non è possibile con questo approccio.

L'inserimento delle dipendenze consente di risolvere questi problemi tramite:

  • L'uso di un'interfaccia o di una classe di base per astrarre l'implementazione delle dipendenze.
  • La registrazione della dipendenza in un contenitore di servizi. 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.
  • L'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.

Ad esempio, l'interfaccia IMessageWriter definisce il metodo Write:

namespace DependencyInjection.Example;

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

Questa interfaccia viene implementata da un tipo concreto, MessageWriter:

namespace DependencyInjection.Example;

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

Il codice di esempio registra il servizio IMessageWriter con il tipo concreto MessageWriter. Il metodo AddSingleton registra il servizio con una durata singleton, la durata dell'app. Le durate dei servizi sono descritte più avanti in questo argomento.

using DependencyInjection.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

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

using IHost host = builder.Build();

host.Run();

Nel codice precedente, l'app di esempio:

  • Crea un'istanza del generatore di app host.

  • Configura i servizi registrando:

    • Oggetto Worker come servizio ospitato. Per altre informazioni, vedere Servizi di lavoro in .NET.
    • Interfaccia IMessageWriter come servizio singleton con un'implementazione corrispondente della classe MessageWriter.
  • Compila l'host e lo esegue.

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.

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

Usando il modello di inserimento delle dipendenze, il servizio di lavoro:

  • Non usa il tipo concreto MessageWriter, solo l'interfaccia IMessageWriter che la implementa. Ciò semplifica la modifica dell'implementazione usata dal servizio di lavoro senza modificare il servizio di lavoro.
  • Non crea un'istanza di MessageWriter. L'istanza viene creata dal contenitore di inserimento delle dipendenze.

L'implementazione dell'interfaccia IMessageWriter può essere migliorata usando l'API di registrazione predefinita:

namespace DependencyInjection.Example;

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

Il metodo AddSingleton aggiornato registra la nuova implementazione IMessageWriter:

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

Il tipo HostApplicationBuilder (builder) fa parte del pacchetto NuGet Microsoft.Extensions.Hosting.

LoggingMessageWriter dipende da ILogger<TCategoryName>, che viene richiesto nel costruttore. ILogger<TCategoryName> è un servizio fornito dal framework.

Non è insolito usare l'inserimento delle dipendenze in modo concatenato. Ogni dipendenza richiesta richiede a sua volta le proprie dipendenze. Il contenitore risolve le dipendenze nel grafico e restituisce il servizio completamente risolto. Il set di dipendenze che devono essere risolte viene generalmente chiamato albero delle dipendenze o grafico dipendenze o grafico degli oggetti.

Il contenitore risolve ILogger<TCategoryName> avvalendosi dei tipi aperti (generici), eliminando la necessità di registrare ogni tipo costruito (generico).

Con la terminologia di inserimento delle dipendenze, un servizio:

  • In genere è un oggetto che fornisce un servizio ad altri oggetti, ad esempio il servizio IMessageWriter.
  • Non è correlato a un servizio Web, anche se il servizio può usare un servizio Web.

Il framework fornisce un sistema di registrazione affidabile. Le implementazioni IMessageWriter illustrate negli esempi precedenti sono state scritte per illustrare l'inserimento delle dipendenze di base, non per implementare la registrazione. La maggior parte delle app non deve scrivere logger. Il codice seguente illustra l'uso della registrazione predefinita, che richiede solo l’oggetto Worker per essere registrato come servizio ospitato 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);
        }
    }
}

Usando il codice precedente, non è necessario aggiornare Program.cs, perché la registrazione viene fornita dal framework.

Regole di individuazione di più costruttori

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 C# seguente:

public class ExampleService
{
    public ExampleService()
    {
    }

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

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

Nel codice precedente presupporre che la registrazione sia stata aggiunta e sia risolvibile dal provider di servizi, ma che i FooService tipi e BarService non lo siano. Il costruttore con il parametro ILogger<ExampleService> viene usato per risolvere l'istanza di ExampleService. Anche se è presente un costruttore che definisce più parametri, i tipi FooService e BarService non sono risolvibili dall'inserimento delle dipendenze.

Se si verificano ambiguità durante l'individuazione dei costruttori, viene generata un'eccezione. Si consideri il servizio di esempio C# seguente:

public class ExampleService
{
    public ExampleService()
    {
    }

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

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

Avviso

Il codice ExampleService con parametri di tipo ambiguo risolvibili con l'inserimento delle dipendenze genererebbe un'eccezione. Non eseguire questa operazione: serve a mostrare cosa si intende per "tipi ambigui risolvibile con l'inserimento delle dipendenze".

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 inserimento delle dipendenze tenta di risolvere il tipo ExampleService, genererà un'eccezione, perché i due costruttori sono ambigui.

È possibile evitare ambiguità definendo un costruttore che accetta entrambi i tipi risolvibili con l'inserimento delle dipendenze:

public class ExampleService
{
    public ExampleService()
    {
    }

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

Registrare gruppi di servizi con metodi di estensione

Le estensioni Microsoft usano una convenzione per la registrazione di un gruppo di servizi correlati. La convenzione consiste nell'usare un singolo metodo di estensione Add{GROUP_NAME} per registrare tutti i servizi richiesti da una funzionalità del framework. Ad esempio, il metodo di estensione AddOptions registra tutti i servizi necessari per l'uso delle opzioni.

Servizi forniti dal framework

Quando si usa uno dei modelli di host o generatore di app disponibili, vengono applicate le impostazioni predefinite e i servizi vengono registrati dal framework. Considerare alcuni dei modelli di host e generatore di app più diffusi:

Dopo aver creato un generatore da una di queste API, IServiceCollection dispone di servizi definiti dal framework, a seconda di come è stato configurato l'host. Per le app basate sui modelli .NET, il framework potrebbe registrare centinaia di servizi.

La tabella seguente elenca un piccolo esempio di questi servizi registrati dal framework:

Tipo di servizio Durata
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> Temporaneo
Microsoft.Extensions.Options.IOptions<TOptions> Singleton
System.Diagnostics.DiagnosticListener Singleton
System.Diagnostics.DiagnosticSource Singleton

Durate del servizio

I servizi possono essere registrati con una delle durate seguenti:

Le sezioni seguenti descrivono ognuna delle durate precedenti. Scegliere una durata appropriata per ogni servizio registrato.

Temporaneo

I servizi con durata temporanea vengono creati ogni volta che vengono richiesti dal contenitore dei servizi. Per registrare un servizio come temporaneo, chiamare AddTransient.

Nelle app che elaborano le richieste, i servizi temporanei vengono eliminati alla fine della richiesta. Questa durata comporta allocazioni per/richiesta, perché i servizi vengono risolti e costruiti ogni volta. Per altre informazioni, vedere Linee guida per l'inserimento delle dipendenze: Linee guida IDisposable per le istanze temporanee e condivise.

Con ambito

Per le applicazioni Web, una durata con ambito indica che i servizi vengono creati una volta per ogni richiesta client (connessione). Registrare i servizi con ambito con AddScoped.

Nelle app che elaborano le richieste, i servizi con ambito vengono eliminati alla fine della richiesta.

Quando si usa Entity Framework Core, il metodo di estensione AddDbContext registra tipi DbContext con una durata con ambito per impostazione predefinita.

Nota

Non risolvere un servizio con ambito da un singleton e prestare attenzione a non farlo indirettamente, ad esempio tramite un servizio temporaneo. Ciò potrebbe causare uno stato non corretto per il servizio durante l'elaborazione delle richieste successive. Va bene:

  • Risolvere un servizio singleton da un servizio con ambito o temporaneo.
  • Risolvere un servizio con ambito da un altro servizio con ambito o temporaneo.

Per impostazione predefinita, nell'ambiente di sviluppo la risoluzione di un servizio da un altro servizio con una durata più lunga genera un'eccezione. Per ulteriori informazioni, vedere Convalida dell'ambito.

Singleton

Vengono creati i servizi di durata Singleton:

  • La prima volta che vengono richiesti.
  • Dallo sviluppatore, quando si fornisce un'istanza di implementazione direttamente al contenitore. Questo approccio è raramente necessario.

Ogni richiesta successiva dell'implementazione del servizio dal contenitore di inserimento delle dipendenze usa la stessa istanza. Se l'app richiede un comportamento singleton, consentire al contenitore del servizio di gestire la durata del servizio. Non implementare il modello di progettazione singleton e fornire il codice per eliminare il singleton. I servizi non devono mai essere eliminati dal codice che ha risolto il servizio dal contenitore. Se un tipo o una factory viene registrato come singleton, il contenitore elimina automaticamente il singleton.

Registrare i servizi singleton con AddSingleton. I servizi Singleton devono essere thread-safe e vengono spesso usati nei servizi senza stato.

Nelle app che elaborano le richieste, i servizi singleton vengono eliminati quando viene eliminato ServiceProvider all'arresto dell'applicazione. Poiché la memoria non viene rilasciata finché l'app non viene arrestata, prendere in considerazione l'uso della memoria con un servizio singleton.

Metodi di registrazione del servizio

Il framework fornisce metodi di estensione per la registrazione del servizio utili in scenari specifici:

metodo Automatico
oggetto
eliminazione
Multipla
Implementazioni
Pass args
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>()

Esempio:

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

Esempi:

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

Esempio:

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

Esempi:

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

Esempi:

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

Per ulteriori informazioni sull'eliminazione dei tipi, vedere la sezione Eliminazione dei servizi.

La registrazione di un servizio con solo un tipo di implementazione equivale alla registrazione del servizio con lo stesso tipo di implementazione e di servizio. Ecco perché non è possibile registrare più implementazioni di un servizio usando i metodi che non accettano un tipo di servizio esplicito. Questi metodi possono registrare più istanze di un servizio, ma avranno tutti lo stesso tipo di implementazione.

Uno dei metodi di registrazione del servizio precedenti può essere usato per registrare più istanze del servizio dello stesso tipo di servizio. Nell'esempio seguente AddSingleton viene chiamato due volte con IMessageWriter come tipo di servizio. La seconda chiamata a AddSingleton esegue l'override di quella precedente quando viene risolta come IMessageWriter e si aggiunge a quella precedente quando più servizi vengono risolti tramite IEnumerable<IMessageWriter>. I servizi vengono visualizzati nell'ordine in cui sono stati registrati quando sono stati risolti tramite 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();

Il codice sorgente di esempio precedente registra due implementazioni di 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);
    }
}

ExampleService definisce due parametri del costruttore; un singolo oggetto IMessageWriter e un oggetto IEnumerable<IMessageWriter>. Il singolo oggetto IMessageWriter è l'ultima implementazione da registrare, mentre IEnumerable<IMessageWriter> rappresenta tutte le implementazioni registrate.

Il framework fornisce anche metodi di estensione TryAdd{LIFETIME}, che registrano il servizio solo se non è già stata registrata un'implementazione.

Nell'esempio seguente la chiamata a AddSingleton registra ConsoleMessageWriter come implementazione per IMessageWriter. La chiamata a TryAddSingleton non ha alcun effetto perché IMessageWriter ha già un'implementazione registrata:

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

TryAddSingleton non ha alcun effetto, perché è già stato aggiunto e il "try" avrà esito negativo. ExampleService asserisce quanto segue:

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

Per altre informazioni, vedi:

I metodi TryAddEnumerable(ServiceDescriptor) registrano il servizio solo se non esiste già un'implementazione dello stesso tipo. Più servizi vengono risolti tramite IEnumerable<{SERVICE}>. Quando si registrano i servizi, aggiungere un'istanza se uno degli stessi tipi non è già stato aggiunto. Gli autori della libreria usano TryAddEnumerable per evitare di registrare più copie di un'implementazione nel contenitore.

Nell'esempio seguente la prima chiamata a TryAddEnumerable registra MessageWriter come implementazione per IMessageWriter1. La seconda chiamata registra MessageWriter per IMessageWriter2. La seconda riga non ha alcun effetto perché IMessageWriter1 dispone già di un'implementazione registrata di 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>());

La registrazione del servizio è in genere indipendente dall'ordine, tranne quando si registrano più implementazioni dello stesso tipo.

IServiceCollection è una raccolta di oggetti ServiceDescriptor. Nell'esempio seguente viene illustrato come registrare un servizio creando e aggiungendo un oggetto ServiceDescriptor:

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

services.Add(descriptor);

I metodi Add{LIFETIME} predefiniti usano lo stesso approccio. Per esempio, vedere il codice sorgente AddScoped.

Comportamento dell'inserimento del costruttore

I servizi possono essere risolti tramite:

I costruttori possono accettare argomenti non forniti tramite l'inserimento di dipendenze, ma gli argomenti devono assegnare valori predefiniti.

Quando i servizi vengono risolti da IServiceProvider o ActivatorUtilities, l'inserimento del costruttore richiede un costruttore pubblico.

Quando i servizi vengono risolti da ActivatorUtilities, l'inserimento del costruttore richiede l'esistenza di un solo costruttore applicabile. 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.

Convalida dell'ambito

Quando l'app viene eseguita nell'ambiente Development 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.

Il provider di servizi radice venga creato con la chiamata di BuildServiceProvider. La durata del provider di servizi radice corrisponde alla durata dell'app quando il provider inizia con l'app e viene eliminato quando l'app viene arrestata.

I servizi con ambito vengono eliminati 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.

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 oggetto IServiceProvider, sarà 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 servizi aggiuntivi.
  • 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

A partire da .NET 8, è disponibile il supporto per le registrazioni e le ricerche del servizio in base a una chiave, ovvero è possibile registrare più servizi con una chiave diversa 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, può essere qualsiasi object desiderato, purché il tipo implementi correttamente Equals.

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...
    }
}

Vedi anche