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 :
- Singleton
- Scoped
- 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 :
// 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 :
class SingletonImplementation: ISingleton
{
var callMeSingleton = ""
// Autre implémentation
public SetSingleton(string value)
{
callMeSingleton = value;
}
// Autre implémentation
}
- Implémentation de la classe Transient :
class TransientImplementation: ITransient
{
var callMeTransient = ""
// Autre implémentation
public SetTransient(string value)
{
callMeTransient = value;
}
// Autre implémentation
}
- Implémentation de la classe Scoped :
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 :
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
:
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
:
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
:
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 serviceTransientImplementation
.ClassB
– Elle conservera la même instance que les autres classes pour le serviceTransientImplementation
.ClassC
– Elle conservera la même instance que les autres classes pour le serviceTransientImplementation
.
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 deTransientImplementation
.ClassB
– Elle aura sa propre instance de service deTransientImplementation
.ClassC
– Elle aura sa propre instance de service deTransientImplementation
.
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
:
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 deTransientImplementation
.ClassB
– Elle aura la même instance du service deTransientImplementation
queClassA
.ClassC
– Elle aura la même instance du service deTransientImplementation
queClassA
etClassB
.
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.
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 :
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