Comprendere il ciclo di vita della dependency injection: singleton, scoped e transient con esempi dettagliati

I Sviluppatori potrebbero essere a conoscenza del ciclo di vita delle istanze di servizio quando utilizzano la iniezione di dipendenze, ma molti non comprendono appieno come funziona. Si possono trovare numerosi articoli online che chiariscono questi concetti, ma spesso si tratta solo di ripetere definizioni che forse già conoscete. Permettetemi di illustrare con un esempio dettagliato che semplifica l’explicazione.

nell’implementazione dell’iniezione di dipendenze, gli sviluppatori hanno tre opzioni che determinano il ciclo di vita delle istanze:

  1. Singleton
  2. Scoped
  3. Transient

Mentre la maggior parte degli sviluppatori riconosce questi termini, un numero significativo ha difficoltà a determinare qual’opzione scegliere per il ciclo di vita di un servizio.

Definizioni

Permettetemi di iniziare con le definizioni:

  • Singleton il servizio di istanza con ciclo di vita creato una sola volta per applicazione dal contenitore di servizi. Un’unica istanza sarà in grado di servire tutte le richieste successive. I servizi Singleton sono disposti alla fine dell’applicazione (cioè al riavvio dell’applicazione).
  • Transient il servizio di istanza con ciclo di vita creato per ogni richiesta dal contenitore di servizi. I servizi Transient sono disposti alla fine della richiesta.
  • Scoped il servizio di istanza con ciclo di vita creato una sola volta per richiesta cliente. I servizi Transient sono disposti alla fine della richiesta.

Quando usare

  • Singleton – Quando si desidera utilizzare istanze singole dei servizi per tutta la durata dell’applicazione
  • Transient – Quando si desidera utilizzare istanze individuali dei servizi all’interno della richiesta del client
  • Scoped – Quando si desidera utilizzare un’unica istanza di servizio per ogni richiesta

Cosa è una richiesta client? In parole semplicissime, puoi considerarla una chiamata API/REST che arriva alla tua applicazione attraverso i clic del pulsante dell’utente per ottenere la risposta.

Non preoccuparti, diamo un’occhiata a un esempio.

Esempio

Prima, creiamo interfacce/servizi e classi:

C#

 

	// dichiariamo 3 servizi come di seguito
		Public interface ISingleton
		Public interface ITransient 
		Public interface IScoped 

Ora scriviamo l’implementazione per ciascun servizio Interfaccia/servizio creato sopra. Cercheremo di capire il concetto provando a aggiornare la variabile callMeSingleton, callMeTransient, e callMeScoped.

  • Implementazione classe Singleton:
C#

 

class SingletonImplementation: ISingleton
{
	var callMeSingleton = ""

	// altra implementazione
	public SetSingleton(string value)
	{
		callMeSingleton = value;
	}
	// altra implementazione
}

  • Implementazione classe Transient:
C#

 

class TransientImplementation: ITransient 
{
	var callMeTransient = ""
	
	// altra implementazione
	public SetTransient(string value)
	{
		callMeTransient = value;
	}
	// altra implementazione
}

  • Implementazione classe Scoped:
C#

 

class ScopedImplementation: IScoped 
{
	var callMeScoped = ""
			
	//altra implementazione
	public SetScoped(string value)
	{
		callMeScoped = value;
	}
	//altra implementazione		
}

Registriamoci (ConfigureServices) con l’iniezione di dipendenze (DI) per decidere il ciclo di vita di ciascuna istanza del servizio:

C#

 

services.AddSingleton<ISingleton, SingletonImplementation>();
services.AddTransient<ITransient , TransientImplementation>();
services.AddScoped<IScoped , ScopedImplementation>();

Utilizziamo/chiamiamo questi servizi da 3 classi differenti (ClassA, ClassB, e ClassC) per capire il ciclo di vita di ciascuno di essi:

  • ClassA:
C#

 

public class ClassA
{
	private ISingleton _singleton;
	//costruttore per istanziare 3 servizi diversi che creiamo
	public ClassA(ISingleton singleton,
                  ITransient _transient,
                  IScoped _scoped)
	{
		_singleton = singleton;
	}
		
	public void UpdateSingletonFromClassA()
	{
		_singleton.SetSingleton("I am from ClassA");
	} 
	
  	public void UpdateTransientFromClassA()
    {
		_transient.SetTransient("I am from ClassA");
	} 
	
  	public void UpdateScopedFromClassA()
	{
		_scoped.SetScoped("I am from ClassA");
	}

	//altre implementazioni 
}

  • ClassB:
C#

 

public class ClassB
{
	private ISingleton _singleton;
	//costruttore per istanziare 3 servizi diversi che creiamo
	public ClassB(ISingleton singleton,
                  ITransient _transient,
                  IScoped _scoped)
	{
		_singleton = singleton;
	}
		
	public void UpdateSingletonFromClassB()
	{
		_singleton.SetSingleton("I am from ClassB");
	} 
	
  	public void UpdateTransientFromClassB()
    {
		_transient.SetTransient("I am from ClassB");
	} 
	
  	public void UpdateScopedFromClassB()
	{
		_scoped.SetScoped("I am from ClassB");
	}

	//altre implementazioni 
}

  • ClassC:
C#

 

public class ClassC
{
	private ISingleton _singleton;
	//costruttore per istanziare 3 servizi diversi che creiamo
	public ClassC(ISingleton singleton,
                  ITransient _transient,
                  IScoped _scoped)
	{
		_singleton = singleton;
	}
		
	public void UpdateSingletonFromClassC()
	{
		_singleton.SetSingleton("I am from ClassC");
	} 
	
  	public void UpdateTransientFromClassC()
    {
		_transient.SetTransient("I am from ClassC");
	} 
	
  	public void UpdateScopedFromClassC()
	{
		_scoped.SetScoped("I am from ClassC");
	}

	//altre implementazioni 
}

Analisi

Analizziamo i risultati e il comportamento di ciascuno dei cicli di vita indicate sopra:

Singleton

Tutte le classi (ClassA, ClassB, e ClassC) useranno la stessa istanza singola della classe SingletonImplementation attraverso tutto il ciclo di vita dell’applicazione. Ciò significa che le proprietà, i campi e le operazioni della classe SingletonImplementation saranno condivise tra le istanze usate in tutte le classi chiamanti. Qualsiasi aggiornamento alle proprietà o ai campi sovrascriverà le modifiche precedenti.

Le classi ClassA, ClassB e ClassC stanno utilizzando il servizio SingletonImplementation come istanza singleton e stanno chiamando SetSingleton per aggiornare la variabile callMeSingleton. In questo caso, ci sarà un singolo valore della variabile callMeSingleton per tutte le richieste che cercano di accedere a questa proprietà. Qualsiasi classe lo modifichi per ultimo avrà il valore sovrascritto per callMeSingleton.

  • ClassA – Avrà la sua stessa istanza come altre classi per il servizio TransientImplementation
  • ClassB – Avrà la sua stessa istanza come altre classi per il servizio TransientImplementation.
  • ClassC – Avrà la sua stessa istanza come altre classi per il servizio TransientImplementation.

ClassA, ClassB e ClassC stanno aggiornando la stessa istanza della classe SingletonImplementation, che sovrascriverà il valore di callMeSingleton. Pertanto, fai attenzione quando imposti o aggiorni le proprietà nell’implementazione del servizio singleton.

I servizi singleton vengono deallocati alla fine dell’applicazione (cioè al riavvio dell’applicazione).

Transient

Tutte le classi (ClassA, ClassB e ClassC) useranno le loro singole istanze della classe TransientImplementation. Ciò significa che se una classe richiede proprietà, campi o operazioni della classe TransientImplementation, aggiornerà o sovrascriverà solo i valori delle sue singole istanze. Aggiornamenti alle proprietà o ai campi non sono condivisi con altre istanze di TransientImplementation.

Capiremo:

  • ClassA – Avrà la sua istanza del servizio di TransientImplementation.
  • ClassB – Avrà la sua istanza del servizio di TransientImplementation.
  • ClassC – Avrà la sua istanza del servizio di TransientImplementation

Immaginiamo di avere una ClassD che chiama il servizio transiente dalle istanze di ClassA, ClassB e ClassC. In questo caso, ogni istanza di classe sarebbe trattata come una diversa/sepatare istanza e ogni classe avrebbe il suo valore di callMeTransient. Leggere i commenti in linea sotto per ClassD:

C#

 

public ClassD
{
	// altra implementazione
		
    // La riga di codice seguente aggiornerà il valore di callMeTransient a "Sono da ClassA" solo per l'istanza di ClassA.
    // E non verrà modificato da eventuali chiamate successive dalla Classe B o classe B
	ClassA.UpdateTransientFromClassA(); 		
       
    // La riga di codice seguente aggiornerà il valore di callMeTransient a "Sono da ClassB" solo per l'istanza di ClassB.
    // E non sovrascriverà il valore per l'istanza di classA né verrà modificato da chiamate successive dalla Classe C
	ClassB.UpdateTransientFromClassB(); 
    
    // La riga di codice seguente aggiornerà il valore di callMeTransient a "Sono da ClassC" solo per l'istanza di ClassC.
    // E non sovrascriverà il valore per le istanze di classA e classB né verrà modificato da eventuali chiamate successive da qualsiasi altra classe.
    ClassC.UpdateTransientFromClassC(); 

	// altra implementazione
}

I servizi transitori vengono smaltiti al termine di ogni richiesta. Usa Transient quando vuoi un comportamento senza stato all’interno della richiesta.

Scoped

Tutte le classi (ClassA, ClassB e ClassC) utilizzeranno istanze singole della classe ScopedImplementation per ciascuna richiesta. Ciò significa che le chiamate per proprietà/campi/operazioni sulla classe ScopedImplementation avverranno su un’istanza singola nel contesto della richiesta. Qualsiasi aggiornamento delle proprietà/campi sarà condiviso tra le altre classi.   

Capire meglio:

  • ClassA – Avrà la sua istanza di servizio di TransientImplementation.
  • ClassB – Avrà la stessa istanza del servizio TransientImplementation di ClassA.
  • ClassC – Avrà la stessa istanza del servizio TransientImplementation di ClassA e ClassB.

Supponiamo che tu abbia una ClassD che chiama il servizio a scopo da istanze di ClassA, ClassB e ClassC. In questo caso, ogni classe avrà un’istanza singola della classe ScopedImplementation. Leggi i commenti in linea per ClassD qui sotto.

C#

 

public class ClassD
{
  // altra implementazione
 
  // Il codice sottostante aggiorna il valore di callMeScoped a "Io sono di classe A" per l'istanza di ClasseA
  // Ma poiché è un ciclo di vita Scoped, mantiene un'unica istanza di ImplementazioneScoped
  // Poi può essere sovrascritto dalla prossima chiamata di ClasseB o ClasseC
	ClassA.UpdateScopedFromClassA();  
  
  // Il codice sottostante aggiorna il valore di callMeScoped a "Io sono di classe B" per l'istanza singola di ImplementazioneScoped 
  // E sovrascriverà il valore di callMeScoped anche per l'istanza di ClasseA. 
	ClassB.UpdateScopedFromClassB();
  
  // Ora, se Classe A eseguirà qualunque operazione sulla ImplementazioneScoped,
  // userà le ultime proprietà/valori campo che sono stati sovrascritti da Classe B.
	
  // Il codice sottostante aggiorna il valore di callMeScoped a "Io sono di classe C"
  // E sovrascriverà il valore di callMeScoped anche per le istanze di ClasseA e ClasseB.
	ClassC.UpdateScopedFromClassC(); 
  // Ora, se Classe B o Classe A eseguiranno qualunque operazione sulla ImplementazioneScoped, useranno le ultime proprietà/valori campo che sono stati sovrascritti da Classe C.

    // altra implementazione
}

I servizi Scoped vengono disposti alla fine di ogni richiesta. Utilizzare Scoped quando si desidera un comportamento stateless tra richieste individuali.

Tempo Trivia

Il ciclo di vita di un servizio può essere sovrascritto da un servizio genitore in cui viene inizializzato. Confuso? Permettetemi di spiegare:

Prendiamo lo stesso esempio dalle classi precedenti e inizializziamo i servizi Transient e Scoped da SingletonImplementation (che è un singleton) come illustrato qui sotto. questo avvierà i servizi ITransient e IScoped e sovrascriverà il ciclo di vita di questi per avere un ciclo di vita singleton come servizio genitore. In questo caso, il tuo applicativo non avrà alcun servizio Transient o Scoped (considerando che hai solo questi 3 servizi che abbiamo usato negli esempi).

Leggi le righe nel codice seguente:

C#

 

public class SingletonImplementation: ISingleton
{
	// costruttore per aggiungere l'inizializzazione dei servizi. 
	private readonly ITransient _transient 
	private readonly IScoped _scoped 
		
	SingletonImplementation(ITransient transient, IScoped scoped)
	{
		_transient = transient;  
        // Ora _transient sarebbe un servizio singleton indipendentemente da come è stato registrato come Transient 
		_scoped = scoped;
        // ora scoped sarebbe un servizio singleton indipendentemente da come è stato registrato come Scoped 
	}
    var callMeSingleton = ""
		
	// altra implementazione 
}

Riepilogo

Spero che l’articolo precedente sia utile per capire il topic. Ti consiglio di provare da te stesso con il contesto fornito sopra e non sarai mai più confuso. Singleton è il più semplice da capire perché una volta creata la sua istanza, sarà condivisa attraverso le applicazioni durante il ciclo di vita dell’applicativo. Sulla stessa linea del Singleton, le istanze Scoped imitano lo stesso comportamento ma solo durante il ciclo di vita di una richiesta attraverso l’applicativo. Transient è totalmente stateless, per ogni richiesta e per ogni istanza di classe verrà tenuto il suo proprio istanza del servizio.

Source:
https://dzone.com/articles/understanding-the-dependency-injection-lifecycle