Verständnis des Dependency-Injection-Lebenszyklus: Singleton, Scoped und Transient mit detaillierten Beispielen

Entwickler könnten sich der Lebenszyklus von Dienstinstanzen bewusst sein, wenn sie Dependency Injection verwenden, aber viele verstehen nicht vollständig, wie es funktioniert. Es gibt zahlreiche Artikel online, die diese Konzepte klären, aber oft wiederholen sie lediglich Definitionen, die du vielleicht bereits kennst. Ich möchte mit einem detaillierten Beispiel die Erklärung vereinfachen.

Beim Implementieren von Dependency Injection haben Entwickler drei Optionen, die den Lebenszyklus der Instanzen bestimmen:

  1. Singleton
  2. Scoped
  3. Transient

Obwohl die meisten Entwickler diese Begriffe kennen, ist es vielen schwer, die richtige Option für die Lebensdauer eines Diensts zu wählen.

Definitionen

Lass mich mit den Definitionen beginnen:

  • Singleton -Dienstinstanzen mit Lebenszyklus werden einmal pro Anwendung vom Dienstcontainer erstellt. Eine einzelne Instanz dient allen folgenden Anfragen. Singleton-Dienste werden am Ende der Anwendung gelöscht (d.h., beim Neustart der Anwendung).
  • Transient -Dienstinstanzen mit Lebenszyklus werden pro Anfrage vom Dienstcontainer erstellt. Transiente Dienste werden am Ende der Anfrage gelöscht.
  • Scoped -Dienstinstanzen mit Lebenszyklus werden einmal pro Kundenanfrage erstellt. Transiente Dienste werden am Ende der Anfrage gelöscht.

Wann zu verwenden

  • Singleton – Wenn Sie während der Lebensdauer der Anwendung nur eine Instanz eines Diensts verwenden möchten.
  • Transient – Wenn Sie innerhalb der Clientanfrage einzelne Instanzen von Diensten verwenden möchten.
  • Scoped – Wenn Sie für jede Anforderung eine einzelne Instanz eines Diensts verwenden möchten.

Was ist eine Clientanfrage? In sehr einfachen Worten, können Sie sie als API/REST-Aufruf betrachten, der durch die Klicks eines Benutzers auf Ihre Anwendung kommt, um eine Antwort zu erhalten.

Keine Sorge, lassen Sie uns das mit einem Beispiel verstehen.

Beispiel

Zuerst erstellen wir Interfaces/Dienste und Klassen:

C#

 

	// Wir deklarieren unten drei Dienste
		Public interface ISingleton
		Public interface ITransient 
		Public interface IScoped 

Jetzt schreiben wir die Implementierung für jeden oben erstellten Dienst-Interface. Wir werden versuchen, das Konzept zu verstehen, indem wir versuchen, die Variablen callMeSingleton, callMeTransient und callMeScoped zu aktualisieren.

  • Implementierung der Singleton-Klasse:
C#

 

class SingletonImplementation: ISingleton
{
	var callMeSingleton = ""

	// andere Implementierung
	public SetSingleton(string value)
	{
		callMeSingleton = value;
	}
	// andere Implementierung
}

  • Implementierung der Transient-Klasse:
C#

 

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

  • Implementierung der Scoped-Klasse:
C#

 

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

Lassen Sie uns die Dienste mit DI (Dependency Injection) registrieren (ConfigureServices), um das Lebenszyklus von jeder Dienstinstanz zu bestimmen:

C#

 

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

Lass uns von drei verschiedenen Klassen (KlasseA, KlasseB und KlasseC) diese Dienste verwenden/aufrufen, um das Lebenszyklus jedes Dienstes zu verstehen:

  • KlasseA:
C#

 

public class ClassA
{
	private ISingleton _singleton;
	// Konstruktor, um 3 unterschiedliche Dienste zu instantiieren, die wir erzeugen
	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");
	}

	// Andere Implementierung 
}

  • KlasseB:
C#

 

public class ClassB
{
	private ISingleton _singleton;
	// Konstruktor, um 3 unterschiedliche Dienste zu instantiieren, die wir erzeugen
	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");
	}

	// Andere Implementierung 
}

  • KlasseC:
C#

 

public class ClassC
{
	private ISingleton _singleton;
	// Konstruktor, um 3 unterschiedliche Dienste zu instantiieren, die wir erzeugen
	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");
	}

	// Andere Implementierung 
}

Analyse

Lass uns die Ergebnisse und das Verhalten für jeden Lebenszyklus einer nach dem anderen von oben gegebenen Implementierung analysieren:

Singleton

Alle Klassen (KlasseA, KlasseB und KlasseC) werden die gleiche einzige Instanz der Klasse SingletonImplementation über den kompletten Lebenszyklus der Anwendung verwenden. Dies bedeutet, dass Eigenschaften, Felder und Operationen der Klasse SingletonImplementation zwischen allen Instanzen, die in allen aufrufenden Klassen verwendet werden, geteilt werden. Jedes Update von Eigenschaften oder Feldern überschreibt vorherige Änderungen.

Die Klassen ClassA, ClassB und ClassC verwenden alle den Service SingletonImplementation als Singleton-Instanz und rufen SetSingleton auf, um die Variable callMeSingleton zu aktualisieren. In diesem Fall wird es für alle Anfragen, die auf diese Eigenschaft zugreifen versuchen, nur einen einzigen Wert der Variable callMeSingleton geben. Die Klasse, die als letztes aufgerufen wird, um die Variable zu aktualisieren, wird den Wert von callMeSingleton überschreiben.

  • ClassA – Es wird dieselbe Instanz für den Service TransientImplementation wie andere Klassen haben.
  • ClassB – Es wird dieselbe Instanz für den Service TransientImplementation wie andere Klassen haben.
  • ClassC – Es wird dieselbe Instanz für den Service TransientImplementation wie andere Klassen haben.

ClassA, ClassB und ClassC aktualisieren dieselbe Instanz der Klasse SingletonImplementation, die den Wert von callMeSingleton überschreiben wird. Daher Vorsicht bei der Festlegung oder Aktualisierung von Eigenschaften in der Singleton-Service-Implementierung.

Singleton-Dienste werden am Ende der Anwendung gelöscht (d.h., bei einem Neustart der Anwendung).

Alle Klassen (ClassA, ClassB und ClassC) werden ihre eigenen Instanzen der Klasse TransientImplementation verwenden. Dies bedeutet, dass wenn eine Klasse Eigenschaften, Felder oder Operationen der Klasse TransientImplementation aufruft, werden nur die individuellen Instanzwerte aktualisiert oder überschrieben. Jedes Update von Eigenschaften oder Feldern wird nicht zwischen anderen Instanzen der TransientImplementation geteilt.

Lassen Sie uns verstehen:

  • ClassA – Es wird ihre eigene Instanz des Dienstes von TransientImplementation haben.
  • ClassB – Es wird ihre eigene Instanz des Dienstes von TransientImplementation haben.
  • ClassC – Es wird ihre eigene Instanz des Dienstes von TransientImplementation haben.

Nehmen wir an, Sie haben eine ClassD, die transienten Dienst von den Instanzen von ClassA, ClassB und ClassC aufruft. In diesem Fall wird jede Klasseinstanz als unterschiedliche/separate Instanz behandelt und jeder Klasse hat ihren eigenen Wert für callMeTransient. Lesen Sie die inline Kommentare unten für ClassD:

C#

 

public ClassD
{
	// andere Implementierung
		
    // Der folgende Code zeile wird den Wert von callMeTransient auf "Ich komme aus ClassA" aktualisieren, nur für das einzigartige Beispiel von ClassA.
    // Und er wird nicht durch irgendwelche nächsten Aufrufe von Class B oder B Klasse geändert
	ClassA.UpdateTransientFromClassA(); 		
       
    // Die folgende Code zeile wird den Wert von callMeTransient auf "Ich komme aus ClassB" aktualisieren, nur für das einzigartige Beispiel von ClassB.
    // Und er wird weder den Wert für das ClassA Beispiel überschreiben, noch wird er durch einen nächsten Aufruf von Class C geändert
	ClassB.UpdateTransientFromClassB(); 
    
    // Die folgende Code zeile wird den Wert von callMeTransient auf "Ich komme aus ClassC" aktualisieren, nur für das einzigartige Beispiel von ClassC.
    // Und er wird weder den Wert für die ClassA und ClassB Beispiele überschreiben, noch wird er durch einen irgendwelchen nächsten Aufruf von irgendwelcher anderen Klasse geändert
    ClassC.UpdateTransientFromClassC(); 

	// andere Implementierung
}

Transient Dienstleistungen werden am Ende jedes Anforderungen abgebrochen. Verwenden Sie Transient, wenn Sie ein stateless Verhalten innerhalb der Anforderung wünschen.

Scoped

Alle Klassen (ClassA, ClassB, und ClassC) werden für jede Anforderung Single-Instanzen des ScopedImplementation Klassen verwenden. Dies bedeutet, dass Aufrufe für Eigenschaften/Felder/Operationen auf der ScopedImplementation Klasse auf einer einzigen Instanz innerhalb des Anforderungsbereichs erfolgen. Jeder Update von Eigenschaften/Feldern wird zwischen anderen Klassen geteilt.  

Lassen Sie uns verstehen:

  • ClassA – Es wird eine Instanz des Dienstes von TransientImplementation haben.
  • ClassB – Es wird dieselbe Instanz des Dienstes von TransientImplementation wie ClassA haben.
  • ClassC – Es wird dieselbe Instanz des Dienstes von TransientImplementation wie ClassA und ClassB haben.

Angenommen, Sie haben eine ClassD, die den Scoped-Dienst von Instanzen von ClassA, ClassB und ClassC aufruft. In diesem Fall wird jede Klasse eine einzelne Instanz der ScopedImplementation-Klasse haben. Lesen Sie die Inline-Kommentare für ClassD unten.

C#

 

public class ClassD
{
  // andere Implementierung
 
  // Der folgende Code aktualisiert den Wert von callMeScoped auf "I am from ClassA" für die Instanz von ClassA
  // Da es sich um einen Scoped-Lebenszyklus handelt, hält er eine einzelne Instanz von ScopedImplementation
  // Dieser kann dann durch den nächsten Aufruf von ClassB oder ClassC überschrieben werden
	ClassA.UpdateScopedFromClassA();  
  
  // Der folgende Code aktualisiert den Wert von callMeScoped auf "I am from ClassB" für die einzelne Instanz von ScopedImplementation 
  // Und er überschreibt auch den Wert von callMeScoped für die Instanz von classA. 
	ClassB.UpdateScopedFromClassB();
  
  // Wenn nun Class A einige Operationen auf ScopedImplementation ausführt,
  // verwendet es die neuesten Eigenschaften/ Feldeinträge, die von classB überschrieben wurden.
	
  // Der folgende Code aktualisiert den Wert von callMeScoped auf "I am from ClassC"
  // Und er überschreibt auch die Werte von callMeScoped für die Instanzen von classA und ClassB.
	ClassC.UpdateScopedFromClassC(); 
  // Wenn nun Class B oder Class A Operationen auf ScopedImplementation ausführen, verwenden sie die neuesten Eigenschaften/ Feldeinträge, die von classC überschrieben wurden

    // andere Implementierung
}

Scoped-Dienste werden am Ende jeder Anfrage freigegeben. Verwenden Sie Scoped, wenn Sie ein stateless Verhalten zwischen einzelnen Anfragen wünschen.

Trivia-Zeit

Das Lebenszyklus eines Diensts kann von einem übergeordneten Dienst überschrieben werden, wo er initialisiert wird. Verwirrt? Ich erkläre es Ihnen:

Nehmen wir denselben Beispiel aus den oben genannten Klassen und initialisieren wir die flüchtigen und scoped Dienste aus SingletonImplementation (der ein Singleton ist) wie unten gezeigt. Dies würde die ITransient und IScoped Dienste initialisieren und die Lebenszyklen dieser auf den Singleton-Lebenszyklus des übergeordneten Dienstes ändern. In diesem Fall hätte Ihre Anwendung keine flüchtigen oder scoped Dienste mehr (considern Sie, dass Sie nur diese 3 Dienste in unseren Beispielen verwendet haben).

Lesen Sie die Zeilen in dem untenstehenden Code durch:

C#

 

public class SingletonImplementation: ISingleton
{
	// Konstruktor, um die Dienste zu initialisieren.
	private readonly ITransient _transient 
	private readonly IScoped _scoped 
		
	SingletonImplementation(ITransient transient, IScoped scoped)
	{
		_transient = transient;  
        // Jetzt verhalten sich _transient wie ein Singleton-Dienst, egal wie er als flüchtig registriert wurde
		_scoped = scoped;
        // Jetzt verhalten sich scoped wie Singleton-Dienste, egal ob sie als scoped registriert wurden
	}
    var callMeSingleton = ""
		
	// andere Implementierung
}

Zusammenfassung

Ich hoffe, der obige Artikel ist hilfreich, um das Thema zu verstehen. Ich würde es empfehlen, es mit dem oben gegebenen Kontext selbst auszuprobieren und du wirst nie wieder verwirrt sein. Singleton ist die einfachste zu verstehen, weil es einmal eine Instanz erstellt, wird über den gesamten Lebenszyklus der Anwendung in der Anwendung geteilt. Auf der gleichen Art und Weise wie Singleton werden scoped Instanzen das gleiche Verhalten nachahmen, aber nur während des Lebenszyklus einer Anfrage in der Anwendung. Flüchtige sind völlig stateless, für jede Anfrage und jede Klasseinstanz wird ihre eigene Instanz des Dienstes behalten.

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