Linee guida per l'inserimento delle dipendenze

Questo articolo fornisce le linee guida generali e le procedure consigliate per l'implementazione dell'inserimento delle dipendenze nelle applicazioni .NET.

Progettare i servizi per l'inserimento di dipendenze

Quando si progettano servizi per l'inserimento delle dipendenze:

  • Evitare classi e membri statici con stato. Evitare di creare uno stato globale progettando invece le app per l'uso dei servizi singleton.
  • Evitare la creazione diretta di istanze delle classi dipendenti all'interno di servizi. La creazione diretta di istanze associa il codice a una determinata implementazione.
  • Rendere i servizi di dimensioni ridotte, ben fattorizzati e facili da testare.

Se una classe presenta molte dipendenze inserite, potrebbe essere un segno che la classe ha troppe responsabilità e viola il principio di responsabilità singola (SRP). Tentare di effettuare il refactoring della classe spostando alcune delle responsabilità in nuove classi.

Eliminazione dei servizi

Il contenitore è responsabile della pulizia dei tipi creati e richiama Dispose sulle istanze IDisposable. I servizi risolti dal contenitore non devono mai essere eliminati dallo sviluppatore. Se un tipo o una factory viene registrato come singleton, il contenitore elimina automaticamente il singleton.

Nell'esempio seguente i servizi vengono creati dal contenitore del servizio ed eliminati automaticamente:

namespace ConsoleDisposable.Example;

public sealed class TransientDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(TransientDisposable)}.Dispose()");
}

L'elemento eliminabile precedente deve avere una durata temporanea.

namespace ConsoleDisposable.Example;

public sealed class ScopedDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(ScopedDisposable)}.Dispose()");
}

L'elemento eliminabile precedente deve avere una durata con ambito.

namespace ConsoleDisposable.Example;

public sealed class SingletonDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(SingletonDisposable)}.Dispose()");
}

L'elemento eliminabile precedente deve avere una durata singleton.

using ConsoleDisposable.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped<ScopedDisposable>();
builder.Services.AddSingleton<SingletonDisposable>();

using IHost host = builder.Build();

ExemplifyDisposableScoping(host.Services, "Scope 1");
Console.WriteLine();

ExemplifyDisposableScoping(host.Services, "Scope 2");
Console.WriteLine();

await host.RunAsync();

static void ExemplifyDisposableScoping(IServiceProvider services, string scope)
{
    Console.WriteLine($"{scope}...");

    using IServiceScope serviceScope = services.CreateScope();
    IServiceProvider provider = serviceScope.ServiceProvider;

    _ = provider.GetRequiredService<TransientDisposable>();
    _ = provider.GetRequiredService<ScopedDisposable>();
    _ = provider.GetRequiredService<SingletonDisposable>();
}

La console di debug mostra l'output di esempio seguente dopo l'esecuzione:

Scope 1...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

Scope 2...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

info: Microsoft.Hosting.Lifetime[0]
      Application started.Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
     Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
     Content root path: .\configuration\console-di-disposable\bin\Debug\net5.0
info: Microsoft.Hosting.Lifetime[0]
     Application is shutting down...
SingletonDisposable.Dispose()

Servizi non creati dal contenitore del servizio

Osservare il codice seguente:

// Register example service in IServiceCollection
builder.Services.AddSingleton(new ExampleService());

Nel codice precedente:

  • L'istanza ExampleServicenon viene creata dal contenitore del servizio.
  • Il framework non elimina automaticamente i servizi.
  • Lo sviluppatore è responsabile dell'eliminazione dei servizi.

Indicazioni IDisposable per le istanze temporanee e condivise

Durata temporanea e limitata

Scenario

L'app richiede un'istanza IDisposable con durata temporanea per uno degli scenari seguenti:

  • L'istanza viene risolta nell'ambito radice (contenitore radice).
  • L'istanza deve essere eliminata prima del termine dell'ambito.

Soluzione

Utilizzare il criterio factory per creare un'istanza esterna all'ambito principale. In questo caso, l'app in genere avrà un metodo Create che chiama direttamente il costruttore del tipo finale. Se il tipo finale ha altre dipendenze, la factory può:

Istanza condivisa, durata limitata

Scenario

L'app richiede un'istanza IDisposable condivisa tra più servizi, ma l'istanza IDisposable deve avere una durata limitata.

Soluzione

Registrare l'istanza con una durata con ambito. Usare IServiceScopeFactory.CreateScope per creare un nuovo oggetto IServiceScope. Usare l'oggetto IServiceProvider dell’ambito per ottenere i servizi necessari. Eliminare l'ambito quando non è più necessario.

Linee guida generali IDisposable

  • Non registrare IDisposable le istanze con una durata temporanea. Utilizzare invece il criterio factory.
  • Non risolvere le istanze IDisposable con una durata temporanea o con ambito nell'ambito radice. L'unica eccezione è se l'app crea/ricrea ed elimina IServiceProvider, ma questo non rappresenta un criterio ideale.
  • La ricezione di una dipendenza IDisposable tramite l'inserimento delle dipendenze non richiede che il ricevitore implementi IDisposable. Il ricevitore della dipendenza IDisposable non deve richiamare Dispose in relazione a tale dipendenza.
  • Utilizzare gli ambiti per controllare la durata dei servizi. Gli ambiti non sono gerarchici e non esiste una connessione speciale tra gli ambiti.

Per altre informazioni sulla pulizia delle risorse, consultare Implementare un Dispose metodo o Implementare un DisposeAsync metodo. Considerare anche lo scenario Servizi temporanei eliminabili acquisiti dal contenitore in relazione alla pulizia delle risorse.

Sostituzione del contenitore di servizi predefinito

Il contenitore di servizi predefinito è progettato per soddisfare le esigenze del framework e della maggior parte delle app consumer. È consigliabile usare il contenitore predefinito, a meno che non sia necessaria una funzionalità specifica non supportata, come:

  • Inserimento di proprietà
  • Inserimento basato solo sul nome (solo .NET 7 e versioni precedenti. Per altre informazioni, consultare Servizi con chiave.
  • Contenitori figlio
  • Gestione della durata personalizzata
  • Supporto di Func<T> per l'inizializzazione differita
  • Registrazione basata su convenzioni

I contenitori di terze parti seguenti possono essere utilizzati con app ASP.NET Core:

Thread safety

Creare servizi singleton thread-safe. Se un servizio singleton ha una dipendenza in un servizio temporaneo, potrebbe essere necessario che anche il servizio temporaneo sia thread-safe, a seconda di come viene usato dal singleton.

Non è necessario che il metodo factory di un servizio singleton, ad esempio il secondo argomento per AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), sia thread-safe. Come nel caso di un costruttore di tipo (static), è garantito che venga chiamato una sola volta da un singolo thread.

Consigli

  • La risoluzione del servizio basata su async/await e Task non è supportata. Poiché C# non supporta costruttori asincroni, utilizzare i metodi asincroni dopo avere risolto in modo sincrono il servizio.
  • Evitare di archiviare i dati e la configurazione direttamente nel contenitore del servizio. Ad esempio, il carrello acquisti di un utente non dovrebbe in genere essere aggiunto al contenitore del servizio. La configurazione deve usare il modello di opzioni. Analogamente, evitare gli oggetti "contenitori di dati" che hanno la sola funzione di consentire l'accesso ad altri oggetti. È preferibile richiedere l'elemento effettivo tramite inserimento delle dipendenze.
  • Evitare l'accesso statico ai servizi. Ad esempio, evitare di acquisire IApplicationBuilder.ApplicationServices come campo statico o proprietà da usare altrove.
  • Mantenere le factory ID rapide e sincrone.
  • Evitare di usare il modello di localizzatore del servizio. Ad esempio, non richiamare GetService per ottenere un'istanza del servizio quando è invece possibile usare l'inserimento delle dipendenze.
  • Un'altra variazione del localizzatore del servizio da evitare è inserire una factory che risolve le dipendenze in fase di esecuzione. Queste procedure combinano le strategie di inversione del controllo.
  • Evitare chiamate a BuildServiceProvider durante la configurazione dei servizi. La chiamata BuildServiceProvider avviene in genere quando lo sviluppatore desidera risolvere un servizio durante la registrazione di un altro servizio. Usare invece un overload che include IServiceProvider per questo motivo.
  • I servizi temporanei eliminabili vengono acquisiti dal contenitore per l'eliminazione. Ciò può trasformarsi in una perdita di memoria se risolta dal contenitore di primo livello.
  • Abilitare la convalida dell'ambito per assicurarti che l'app non abbia singleton che acquisiscano servizi con ambito. Per ulteriori informazioni, vedere Convalida dell'ambito.

È tuttavia possibile che in alcuni casi queste raccomandazioni debbano essere ignorate. Le eccezioni sono rare e principalmente si tratta di casi speciali all'interno del framework stesso.

L'inserimento di dipendenze è un'alternativa ai modelli di accesso agli oggetti statici/globali. Se l'inserimento di dipendenze viene usato con l'accesso agli oggetti statico i vantaggi non saranno evidenti.

Anti-pattern di esempio

Oltre alle linee guida di questo articolo, esistono diversi anti-pattern che è consigliabile evitare. Alcuni di questi anti-pattern sono informazioni relative allo sviluppo dei runtime stessi.

Avviso

Questi sono esempi di anti-pattern, non copiare il codice, non usare questi criteri (pattern) ed evitare questi criteri a tutti i costi.

Servizi temporanei eliminabili acquisiti dal contenitore

Quando si registrano i servizi temporanei che implementano IDisposable, per impostazione predefinita il contenitore di inserimento delle dipendenze resterà su questi riferimenti su questi riferimenti e nessun(o) Dispose() di essi viene eliminato quando l'applicazione si arresta se sono stati risolti dal contenitore o fino a quando l'ambito non viene eliminato se sono stati risolti da un ambito. Ciò può trasformarsi in una perdita di memoria in caso di risoluzione a livello di contenitore.

Anti-pattern: Transient disposables without dispose. Do not copy!

Nell'anti-pattern precedente, di 1.000 oggetti ExampleDisposable viene creata un’istanza ed essi vengono rooted. Non verranno eliminati finché l'istanza serviceProvider non viene eliminata.

Per altre informazioni sull’esecuzione del debug delle perdite di memoria, consultare Eseguire il debug di una perdita di memoria in .NET.

Le factory di inserimento delle dipendenze asincrone possono causare deadlock

Il termine "factory di inserimento delle dipendenze" si riferisce ai metodi di overload esistenti relativi alla chiamata a Add{LIFETIME}. Sono presenti overload che accettano Func<IServiceProvider, T> dove T è il servizio registrato e il parametro è denominato implementationFactory. implementationFactory può essere fornito come espressione lambda, funzione locale o metodo. Se la factory è asincrona e si utilizza Task<TResult>.Result, si verificherà un deadlock.

Anti-pattern: Deadlock with async factory. Do not copy!

Nel codice precedente viene assegnata un'espressione lambda a implementationFactory laddove il corpo chiama Task<TResult>.Result su un metodo di restituzione Task<Bar>. Questo causa un deadlock. Il metodo GetBarAsync emula semplicemente un'operazione di lavoro asincrona con Task.Delay e quindi chiama GetRequiredService<T>(IServiceProvider).

Anti-pattern: Deadlock with async factory inner issue. Do not copy!

Per altre informazioni sulle indicazioni asincrone, consultare Programmazione asincrona: informazioni importanti e consigli. Per altre informazioni sull’esecuzione del debug dei deadlock, consultare Eseguire il debug di un deadlock in .NET.

Quando si esegue questo anti-pattern e si verifica il deadlock, è possibile visualizzare i due thread in attesa dalla finestra Stack in parallelo di Visual Studio. Per altre informazioni, consultare Visualizzare thread e attività nella finestra Stack in parallelo.

Dipendenza captive

Il termine "dipendenza captive" è stato coniato da Mark Seemanne si riferisce alla configurazione errata della durata del servizio, in cui un servizio di lunga durata contiene una captive di servizio di breve durata.

Anti-pattern: Captive dependency. Do not copy!

Nel codice precedente, Foo viene registrato come singleton e Bar è con ambito, che in superficie sembra valido. Considera tuttavia l'implementazione di Foo.

namespace DependencyInjection.AntiPatterns;

public class Foo(Bar bar)
{
}

L'oggetto Foo richiede un oggettoBar e, poiché Foo è un singleton e Bar è con ambito, si tratta di una configurazione errata. Così come è, verrebbe creata un'istanza di Foo una sola volta e resterebbe Bar per tutta la sua durata, che è più lunga della durata prevista con ambito di Bar. È consigliabile valutare la convalida degli ambiti mediante il passaggio di validateScopes: true a BuildServiceProvider(IServiceCollection, Boolean). Quando si convalidano gli ambiti, si otterrà un oggetto InvalidOperationException con un messaggio simile a "Impossibile utilizzare il servizio con ambito 'Bar' dal singleton 'Foo'".

Per ulteriori informazioni, vedere Convalida dell'ambito.

Servizio con ambito come singleton

Quando si usano servizi con ambito, se non si crea un ambito o all'interno di un ambito esistente, il servizio diventa un singleton.

Anti-pattern: Scoped service becomes singleton. Do not copy!

Nel codice precedente, Bar viene recuperato nell’ambito di un oggetto IServiceScope, il che è corretto. L'anti-pattern è il recupero di Bar al di fuori dell'ambito e la variabile è denominata avoid per indicare quale recupero di esempio non è corretto.

Vedi anche