Comprendre le cycle de vie de l’injection de dépendances : Singleton, Scoped et Transient avec des exemples détaillés

Les développeurs peuvent être conscients du cycle de vie des instances de service lors de l’utilisation de injection de dépendances, mais beaucoup ne comprennent pas véritablement comment ça fonctionne. Vous pouvez trouver de nombreux articles en ligne qui clarifient ces concepts, mais ils répètent souvent des définitions que vous pourriez déjà connaître. Permettez-moi d’illustrer avec un exemple détaillé qui simplifie la explication.

Lors de la mise en œuvre d’injection de dépendances, les développeurs disposent de trois options qui déterminent le cycle de vie des instances :

  1. Singleton
  2. Scoped
  3. Transient

Bien que la plupart des développeurs reconnaissent ces termes, un nombre significatif a du mal à déterminer quelle option choisir pour la durée de vie d’un service.

Définitions

Commencez-je par les définitions :

  • Singleton Les instances de service avec une durée de vie unique sont créées une fois par application à partir du conteneur de services. Une seule instance servira toutes les demandes suivantes. Les services Singleton sont détruits à la fin de l’application (c’est-à-dire au redémarrage de l’application).
  • Transient Les instances de service avec une durée de vie transitoire sont créées par demande à partir du conteneur de services. Les services transitoires sont détruits à la fin de la demande.
  • Scoped Les instances de service avec une durée de vie scopée sont créées une fois par demande client. Les services scopés sont détruits à la fin de la demande.

Quand Utiliser

  • Singleton – Lorsque vous souhaitez utiliser des instances uniques de services pendant tout le cycle de vie de l’application
  • Transient – Lorsque vous souhaitez utiliser des instances individuelles de services au sein de la requête cliente
  • Scoped – Lorsque vous souhaitez utiliser une instance unique de service pour chaque requête

Qu’est-ce qu’une requête cliente ? En termes simples, vous pouvez l’imaginer comme une API/REST qui vient dans votre application à la suite de clics de boutons de l’utilisateur pour obtenir une réponse.

Ne vous inquiétez pas, expliquons-le par un exemple.

Exemple

Tout d’abord, créez des interfaces/services et des classes :

C#

 

	// Nous déclarons 3 services ci-dessous
		Public interface ISingleton
		Public interface ITransient 
		Public interface IScoped 

Maintenant, écrivons l’implémentation de chaque service Interface/service créé ci-dessus. Nous essayons de comprendre la conceptue en essayant de mettre à jour les variables callMeSingleton, callMeTransient et callMeScoped.

  • Implémentation de la classe Singleton :
C#

 

class SingletonImplementation: ISingleton
{
	var callMeSingleton = ""

	// Autre implémentation
	public SetSingleton(string value)
	{
		callMeSingleton = value;
	}
	// Autre implémentation
}

  • Implémentation de la classe Transient :
C#

 

class TransientImplementation: ITransient 
{
	var callMeTransient = ""
	
	// Autre implémentation
	public SetTransient(string value)
	{
		callMeTransient = value;
	}
	// Autre implémentation
}

  • Implémentation de la classe Scoped :
C#

 

class ScopedImplementation: IScoped 
{
	var callMeScoped = ""
			
	// Autre implémentation
	public SetScoped(string value)
	{
		callMeScoped = value;
	}
	// Autre implémentation		
}

Enregistrons (ConfigureServices) avec le DI (Injection de dépendances) pour déterminer le cycle de vie de chaque instance de service :

C#

 

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

Nous allons utiliser/appeler ces services depuis 3 classes différentes (ClassA, ClassB et ClassC) pour comprendre le cycle de vie de chaque service :

  • ClassA :
C#

 

public class ClassA
{
	private ISingleton _singleton;
	// Constructeur pour instancier 3 services différents que nous créons
	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");
	}

	// Autres implémentations 
}

  • ClassB :
C#

 

public class ClassB
{
	private ISingleton _singleton;
	// Constructeur pour instancier 3 services différents que nous créons
	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");
	}

	// Autres implémentations 
}

  • ClassC :
C#

 

public class ClassC
{
	private ISingleton _singleton;
	// Constructeur pour instancier 3 services différents que nous créons
	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");
	}

	// Autres implémentations 
}

Analyse

Analisons les résultats et le comportement pour chaque cycle de vie individuellement à partir de l’implémentation ci-dessus :

Singleton

Toutes les classes (ClassA, ClassB et ClassC) utiliseront toujours la même instance unique de la classe SingletonImplementation tout au long du cycle de vie de l’application. Cela signifie que les propriétés, champs et opérations de la classe SingletonImplementation seront partagés entre les instances utilisées dans toutes les classes appelantes. Toutes les mises à jour des propriétés ou des champs remplaceront les modifications précédentes.

Par exemple, dans le code ci-dessus, ClassA, ClassB et ClassC utilisent tous le service SingletonImplementation en tant qu’instance unique et appellent SetSingleton pour mettre à jour la variable callMeSingleton. Dans ce cas, il y aura une seule valeur pour la variable callMeSingleton pour toutes les requêtes essayant d’accéder à cette propriété. La classe qui l’accède dernièrement pour la mettre à jour écrasera la valeur de callMeSingleton.

  • ClassA – Elle conservera la même instance que les autres classes pour le service TransientImplementation.
  • ClassB – Elle conservera la même instance que les autres classes pour le service TransientImplementation.
  • ClassC – Elle conservera la même instance que les autres classes pour le service TransientImplementation.

ClassA, ClassB et ClassC mettent à jour la même instance de la classe SingletonImplementation, ce qui écrase la valeur de callMeSingleton. Par conséquent, faites attention lors de la mise à jour ou de la définition des propriétés dans l’implémentation du service unique.

Les services uniques sont détruits à la fin de l’application (c’est-à-dire, au redémarrage de l’application).

Transient

Toutes les classes (ClassA, ClassB et ClassC) utiliseront leurs propres instances de la classe TransientImplementation. Cela signifie que si l’une des classes appelle des propriétés, des champs ou des opérations de la classe TransientImplementation, elle ne mettra à jour ou ne remplacera que les valeurs de son instance individuelle. Les mises à jour des propriétés ou des champs ne sont pas partagées entre les autres instances de TransientImplementation.

Faisons le point :

  • ClassA – Elle aura sa propre instance de service de TransientImplementation.
  • ClassB – Elle aura sa propre instance de service de TransientImplementation.
  • ClassC – Elle aura sa propre instance de service de TransientImplementation

Imaginons que vous avez une ClassD qui appelle le service transient des instances de ClassA, ClassB et ClassC. Dans ce cas, chaque instance de classe serait traitée comme une instance distincte/seule, et chaque classe aurait sa propre valeur de callMeTransient. Lisez les commentaires en ligne ci-dessous pour ClassD :

C#

 

public ClassD
{
	// Autres implémentations
		
    // La ligne de code ci-dessous met à jour la valeur de callMeTransient en "Je suis de ClassA" uniquement pour l'instance de ClassA.
    // Elle ne sera pas modifiée par aucune prochaine appel de Class B ou de classe B.
	ClassA.UpdateTransientFromClassA(); 		
       
    // La ligne de code ci-dessous met à jour la valeur de callMeTransient en "Je suis de ClassB" uniquement pour l'instance de ClassB.
    // Elle n'override pas la valeur pour l'instance de ClassA et ne sera pas modifiée par la prochaine appel de Class C.
	ClassB.UpdateTransientFromClassB(); 
    
    // La ligne de code ci-dessous met à jour la valeur de callMeTransient en "Je suis de ClassC" uniquement pour l'instance de ClassC.
    // Elle n'override pas la valeur pour les instances de ClassA et ClassB et ne sera pas modifiée par aucune prochaine appel d'autres classes.
    ClassC.UpdateTransientFromClassC(); 

	// Autres implémentations
}

Les services transients sont détruits à la fin de chaque requête. Utilisez Transient lorsque vous voulez un comportement stateless au sein de la requête.

Scoped

Toutes les classes (ClassA, ClassB, et ClassC) utilisent une seule instance de la classe ScopedImplementation pour chaque requête. Cela signifie que les appels pour les propriétés/champs/opérations sur la classe ScopedImplementation se produiront sur une instance unique dans le scope de la requête. Toutes les mises à jour des propriétés/champs seront partagées entre les autres classes.  

Explications :

  • ClassA – Elle disposera de sa propre instance de service de TransientImplementation.
  • ClassB – Elle aura la même instance du service de TransientImplementation que ClassA.
  • ClassC – Elle aura la même instance du service de TransientImplementation que ClassA et ClassB.

Supposons que vous avez une ClassD qui appelle un service scopé depuis les instances de ClassA, ClassB et ClassC. Dans ce cas, chaque classe aura une seule instance de la classe ScopedImplementation. Lisez les commentaires en ligne pour ClassD ci-dessous.

C#

 

public class ClassD
{
  // Autres implémentations
 
  // Le code ci-dessous met à jour la valeur de callMeScoped pour l'instance de ClassA à "Je suis de ClassA"
  // Cependant, comme c'est un cycle de vie Scoped, il garde une seule instance de ScopedImplementation
  // Il peut donc être remplacé par une prochaine appel depuis ClassB ou ClassC
	ClassA.UpdateScopedFromClassA();  
  
  // Le code ci-dessous met à jour la valeur de callMeScoped pour l'instance unique de ScopedImplementation à "Je suis de ClassB" 
  // Et il écrase également la valeur de callMeScoped pour l'instance de ClassA. 
	ClassB.UpdateScopedFromClassB();
  
  // Maintenant, si Class A effectue une opération sur ScopedImplementation,
  // elle utilisera les dernières propriétés/valeurs de champ qui ont été écrasées par ClassB.
	
  // Le code ci-dessous met à jour la valeur de callMeScoped à "Je suis de ClassC"
  // Et il écrase également la valeur de callMeScoped pour les instances de ClassA et ClassB.
	ClassC.UpdateScopedFromClassC(); 
  // Si Class B ou Class A effectue une opération sur ScopedImplementation, elle utilisera les dernières propriétés/valeurs de champ qui ont été écrasées par ClassC.

    // Autres implémentations
}

Les services scoped sont détruits à la fin de chaque demande. Utilisez Scoped lorsque vous souhaitez un comportement stateless entre les demandes individuelles.

Temps de trivia

Le cycle de vie d’un service peut être écrasé par un service parent où il est initialisé. Confus? Laissez-moi vous expliquer :

Prenons l’exemple précédent des classes et initialisons les services Transient et Scoped à partir de SingletonImplementation (qui est un singleton) comme suit. Cela déclencherait l’initialisation des services ITransient et IScoped et écraserait le cycle de vie de ces services en leur donnant celui du singleton. Dans ce cas, votre application ne disposerait pas de services Transient ou Scoped (en supposant que vous n’avez que ces 3 services que nous avons utilisés dans nos exemples).

Lisez le code ci-dessous :

C#

 

public class SingletonImplementation: ISingleton
{
	// constructeur pour initialiser les services.
	private readonly ITransient _transient 
	private readonly IScoped _scoped 
		
	SingletonImplementation(ITransient transient, IScoped scoped)
	{
		_transient = transient;  
        // Maintenant, _transient serait un service singleton indépendamment de la manière dont il a été enregistré comme Transient
		_scoped = scoped;
        // maintenant, scoped serait un service singleton indépendamment de son enregistrement en tant que Scoped
	}
    var callMeSingleton = ""
		
	// autres implémentations
}

Résumé

J’espère que l’article précédent est utile pour comprendre le sujet. Je vous recommanderais d’essayer vous-même avec le contexte ci-dessus et vous ne serez jamais dépassé par la suite. Le singleton est le plus facile à comprendre parce qu’une fois qu’une instance est créée, elle sera partagée à travers les applications pendant tout le cycle de vie de l’application. De même, les instances scoped imitent le même comportement mais uniquement pendant le cycle de vie d’une requête dans l’application. Les services Transient sont complètement stateless, chacune des instances de service est propre à chaque requête et chaque instance de classe en garde sa propre instance de service.

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