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.
Questo articolo fornisce le linee guida generali e le procedure consigliate per l'implementazione dell'inserimento delle dipendenze nelle applicazioni .NET.
Servizi di progettazione per l'iniezione di dipendenze
Quando si progettano servizi per l'inversione 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. L'istanza diretta associa il codice a una particolare 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 gestisce la 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 fabbrica viene registrato come singleton, il contenitore gestisce 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()");
}
Il prodotto monouso precedente è destinato ad avere una durata di vita transitoria.
namespace ConsoleDisposable.Example;
public sealed class ScopedDisposable : IDisposable
{
public void Dispose() => Console.WriteLine($"{nameof(ScopedDisposable)}.Dispose()");
}
L'oggetto precedente è destinato ad avere una durata limitata al contesto.
namespace ConsoleDisposable.Example;
public sealed class SingletonDisposable : IDisposable
{
public void Dispose() => Console.WriteLine($"{nameof(SingletonDisposable)}.Dispose()");
}
L'oggetto monouso precedente è progettato per 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
ExampleService
non viene creata dal contenitore del servizio. - Il framework non elimina automaticamente i servizi.
- Lo sviluppatore è responsabile dello smaltimento dei servizi.
Linee guida 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 nel contenitore radice (ambito radice).
- L'istanza deve essere smaltita 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ò:
- Ricevere un oggetto IServiceProvider nel relativo costruttore.
- Usare ActivatorUtilities.CreateInstance per creare un'istanza all'esterno del contenitore, usando il contenitore per le relative dipendenze.
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 limitato. 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 durata transitoria. Utilizzare invece il modello factory.
- Non risolvere le istanze IDisposable con una durata di vita temporanea o durata di vita limitata nel contesto 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 Dependency Injection non richiede che il ricevitore implementi IDisposable da solo. 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. Inoltre, considera lo scenario dei servizi temporanei eliminabili gestiti 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:
- Iniezione 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 per
Func<T>
nell'inizializzazione pigra - Registrazione basata su convenzioni
I contenitori di terze parti seguenti possono essere utilizzati con app ASP.NET Core:
Sicurezza dei thread
Creare servizi singleton thread-safe. Se un servizio singleton ha una dipendenza da un servizio temporaneo, potrebbe essere necessario che anche il servizio temporaneo sia thread-safe, a seconda di come viene utilizzato 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
eTask
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. È meglio richiedere l'elemento effettivo tramite DI.
- Evitare l'accesso statico ai servizi. Ad esempio, evitare di acquisire IApplicationBuilder.ApplicationServices come campo statico o proprietà da usare altrove.
- Mantieni le fabbriche DI veloci e sincronizzate.
- Evitare di usare il modello di localizzatore del servizio. Ad esempio, non richiamare GetService per ottenere un'istanza del servizio quando è invece possibile usare Dependency Injection.
- Un'altra variazione del localizzatore di servizi da evitare è iniettare una factory che risolve le dipendenze al runtime. 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. Invece, utilizza un overload che includaIServiceProvider
per questo motivo. - I servizi temporanei eliminabili vengono acquisiti dal contenitore per l'eliminazione. Ciò può trasformarsi in una perdita di memoria se gestito a partire dal contenitore principale.
- 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.
Come per tutti i set di raccomandazioni, può esserci la necessità di ignorare una raccomandazione in determinate situazioni. Le eccezioni sono rare e principalmente si tratta di casi speciali all'interno del framework stesso.
Inserimento di dipendenze è alternativa ai modelli di accesso agli oggetti statici/globali. Potresti non riuscire a realizzare i benefici del DI se lo mescoli con l'accesso agli oggetti statici.
Esempi di anti-pattern
Oltre alle linee guida di questo articolo, esistono diversi anti-pattern che è consigliabile evitare. Alcuni di questi anti-pattern sono insegnamenti tratti dallo sviluppo dei runtime stessi.
Avviso
Questi sono esempi di anti-pattern, non copiare il codice, non usare questi schemi ed evitare questi schemi a tutti i costi.
Servizi temporanei eliminabili acquisiti dal contenitore
Quando si registrano i servizi transitori che implementano IDisposable, per impostazione predefinita il contenitore di Dependency Injection manterrà questi riferimenti e non li eliminerà fino a quando il contenitore non viene smaltito all'arresto dell'applicazione, se sono stati risolti dal contenitore, oppure fino a quando l'ambito non viene smaltito, se sono stati risolti da un ambito. Ciò può trasformarsi in una perdita di memoria in caso di risoluzione a livello di contenitore.
Nell'anti-pattern precedente, vengono create istanze di 1.000 oggetti ExampleDisposable
e vengono radicati. 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 fabbriche asincrone di inserimento delle dipendenze possono causare stalli
Il termine "fabbriche DI" si riferisce ai metodi sovraccarico che esistono quando si chiama 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, ciò causerà un deadlock.
Nel codice precedente, a implementationFactory
viene assegnata un'espressione lambda in cui il corpo chiama Task<TResult>.Result su un metodo che restituisce 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).
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 Pile Parallele di Visual Studio. Per altre informazioni, consultare Visualizzare thread e attività nella finestra Stack in parallelo.
Dipendenza captive
Il termine "dipendenza captiva" è stato coniato da Mark Seemann e si riferisce alla configurazione errata della durata di vita dei servizi, in cui un servizio di lunga durata trattiene un servizio di breve durata come captivo.
Nel codice precedente, Foo
viene registrato come singleton e Bar
è oggetto di ambito, che a prima vista sembra valido. Considera tuttavia l'implementazione di Foo
.
namespace DependencyInjection.AntiPatterns;
public class Foo(Bar bar)
{
}
L'oggetto Foo
richiede un oggetto Bar
e, poiché Foo
è un singleton e Bar
è scopato, si tratta di una configurazione errata. Così com'è, Foo
verrebbe istanziato una sola volta e manterrebbe il riferimento a Bar
per tutto il suo ciclo di vita, che è più lungo del ciclo di vita previsto con ambito di Bar
. È consigliabile considerare 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 nuovo ambito o non si è all'interno di un ambito esistente, il servizio diventa un singleton.
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.