Entendendo o Ciclo de Vida da Injeção de Dependência: Singleton,Scoped e Transient com Exemplos Detalhados

Desenvolvedores podem estar cientes do ciclo de vida das instâncias de serviço quando usam injeção de dependência, mas muitos não entendem plenamente como funciona. Você pode encontrar numerosos artigos online que esclarecem esses conceitos, mas eles muitas vezes apenas reiteram definições que você já pode saber. Deixe-me ilustrar com um exemplo detalhado que simplifica a explicação.

Ao implementar a injeção de dependência, os desenvolvedores têm três opções que determinam o ciclo de vida das instâncias:

  1. Singleton
  2. Scoped
  3. Transient

Enquanto a maioria dos desenvolvedores reconhece esses termos, um número significativo tem dificuldade em decidir qual opção escolher para o tempo de vida de um serviço.

Definições

Comece com as definições:

  • Singleton instâncias de tempo de vida de serviço são criadas uma vez por aplicação a partir do container de serviços. Uma única instância servirá todas as solicitações subsequentes. Os serviços Singleton são descartados no final da aplicação (isto é, ao reiniciar a aplicação).
  • Transient instâncias de tempo de vida de serviço são criadas por solicitação por meio do container de serviços. Serviços transientes são descartados no final da solicitação.
  • Scoped instâncias de tempo de vida de serviço são criadas uma vez por solicitação de cliente. Serviços transientes são descartados no final da solicitação.

Quando Usar

  • Singleton  – Quando você deseja usar instâncias únicas de serviços durante todo o ciclo de vida da aplicação
  • Transient  – Quando você deseja usar instâncias individuais de serviços dentro da solicitação do cliente
  • Scoped – Quando você deseja usar uma única instância de serviço para cada solicitação

O que é uma solicitação de cliente? Em palavras muito simples, você pode considerá-la como uma chamada API/REST chegando à sua aplicação por cliques de botão do usuário para obter a resposta.

Não se preocupe, vamos entender com um exemplo.

Exemplo

Primeiro, vamos criar interfaces/serviços e classes:

C#

 

	// estamos declarando 3 serviços conforme abaixo
		Public interface ISingleton
		Public interface ITransient 
		Public interface IScoped 

Agora vamos escrever a implementação para cada serviço Interface/serviço criado acima. Vamos tentar entender o conceito tentando atualizar as variáveis callMeSingleton, callMeTransient e callMeScoped.

  • Implementação da classe Singleton:
C#

 

class SingletonImplementation: ISingleton
{
	var callMeSingleton = ""

	// outra implementação
	public SetSingleton(string value)
	{
		callMeSingleton = value;
	}
	// outra implementação
}

  • Implementação da classe Transient:
C#

 

class TransientImplementation: ITransient 
{
	var callMeTransient = ""
	
	// outra implementação
	public SetTransient(string value)
	{
		callMeTransient = value;
	}
	// outra implementação
}

  • Implementação da classe Scoped:
C#

 

class ScopedImplementation: IScoped 
{
	var callMeScoped = ""
			
	//outra implementação
	public SetScoped(string value)
	{
		callMeScoped = value;
	}
	//outra implementação		
}

Vamos registrar (ConfigureServices) com DI (Injeção de Dependência) para decidir o ciclo de vida de cada instância de serviço:

C#

 

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

`

Usemos estes serviços de 3 classes diferentes (ClassA, ClassB e ClassC) para entender o ciclo de vida de cada serviço:

  • ClassA:
C#

 

public class ClassA
{
	private ISingleton _singleton;
	// construtor para instanciar 3 serviços diferentes que criamos
	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");
	}

	// outras implementações 
}

  • ClassB:
C#

 

public class ClassB
{
	private ISingleton _singleton;
	// construtor para instanciar 3 serviços diferentes que criamos
	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");
	}

	// outras implementações 
}

  • ClassC:
C#

 

public class ClassC
{
	private ISingleton _singleton;
	// construtor para instanciar 3 serviços diferentes que criamos
	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");
	}

	// outras implementações 
}

Análise

Analisemos os resultados e o comportamento de cada ciclo de vida um a um a partir da implementação acima:

Singleton

Todas as classes (ClassA, ClassB e ClassC) usarão a mesma instância única da classe SingletonImplementation ao longo do ciclo de vida da aplicação. Isso significa que propriedades, campos e operações da classe SingletonImplementation serão compartilhados entre as instâncias usadas em todas as classes de chamada. Qualquer atualização de propriedades ou campos substituirá as alterações anteriores.

Por exemplo, no código acima, ClassA, ClassB e ClassC estão todos utilizando o serviço SingletonImplementation como uma instância única e chamando SetSingleton para atualizar a variável callMeSingleton. Neste caso, haverá um valor único para a variável callMeSingleton para todas as solicitações tentando acessar essa propriedade. A classe que acessa última para atualizar substituirá o valor de callMeSingleton.

  • ClassA – terá a mesma instância dos outros classes para o serviço TransientImplementation.
  • ClassB – terá a mesma instância dos outros classes para o serviço TransientImplementation.
  • ClassC – terá a mesma instância dos outros classes para o serviço TransientImplementation.

ClassA, ClassB e ClassC estão atualizando a mesma instância da classe SingletonImplementation, o que substituirá o valor de callMeSingleton. Portanto, tenha cuidado quando definir ou atualizando propriedades na implementação de serviço de singleton.

Serviços singletons são descartados no final do aplicativo (ou seja, após o reinício do aplicativo).

Transiente

Todas as classes (ClassA, ClassB, e ClassC) usarão suas instâncias individuais da classe TransientImplementation. Isso significa que se uma classe chamar por propriedades, campos ou operações da classe TransientImplementation, ela apenas atualizará ou sobrescreverá seus valores de instância individuais. Qualquer atualização de propriedades ou campos não é compartilhada entre outras instâncias de TransientImplementation.

Vamos entender:

  • ClassA – Ela terá sua própria instância do serviço de TransientImplementation.
  • ClassB – Ela terá sua própria instância do serviço de TransientImplementation.
  • ClassC – Ela terá sua própria instância do serviço de TransientImplementation

Pense em você ter uma ClassD que está chamando um serviço transiente de ClassA, ClassB, e ClassC instâncias. Neste caso, cada instância de classe seria tratada como uma instância diferente/separada e cada classe teria seu próprio valor de callMeTransient. Leia os comentários embutidos abaixo para ClassD:

C#

 

public ClassD
{
	// outra implementação
		
    // A linha de código abaixo irá atualizar o valor de callMeTransient para "I am from ClassA" apenas para a instância de ClassA.
    // E não será alterado por quaisquer chamadas subsequentes de Class B ou B class
	ClassA.UpdateTransientFromClassA(); 		
       
    // A linha de código abaixo irá atualizar o valor de callMeTransient para "I am from ClassB" apenas para a instância de ClassB.
    // E não sobrescreverá o valor para a instância de calssA nem será alterado por chamadas subsequentes de Class C
	ClassB.UpdateTransientFromClassB(); 
    
    // A linha de código abaixo irá atualizar o valor de callMeTransient para "I am from ClassC" apenas para a instância de ClassC.
    // E não sobrescreverá o valor para as instâncias de calssA e classB nem será alterado por quaisquer chamadas subsequentes de qualquer outra classe.
    ClassC.UpdateTransientFromClassC(); 

	// outra implementação
}

Os serviços transientes são descartados no final de cada solicitação. Use Transient quando você quiser um comportamento state less dentro da solicitação.

Scoped

Todas as classes (ClassA, ClassB, e ClassC) usarão instâncias únicas de ScopedImplementation para cada solicitação. Isso significa que as chamadas para propriedades/campos/operações na classe ScopedImplementation acontecerão em uma única instância dentro do escopo da solicitação. Qualquer atualização de propriedades/campos será compartilhada entre outras classes.   

Vamos entender:

  • ClassA – Terá sua instância do serviço de TransientImplementation.
  • ClassB – Terá a mesma instância de serviço de TransientImplementation que ClassA.
  • ClassC – Terá a mesma instância de serviço de TransientImplementation que ClassA e ClassB.

Digamos que você tem uma ClassD que está chamando um serviço de escopo de ClassA, ClassB e instâncias de ClassC. Neste caso, cada classe terá uma única instância da classe ScopedImplementation. Leia os comentários em linha para ClassD abaixo.

C#

 

public class ClassD
{
  // outra implementação
 
  // O código abaixo atualizará o valor de callMeScoped para "Eu sou do ClassA" para a instância de ClassA
  // Mas como é um ciclo de vida Scoped, ele mantém uma única instância de ScopedImplementation
  // Em seguida, ele pode ser sobrescrito por próximas chamadas de ClassB ou ClassC
	ClassA.UpdateScopedFromClassA();  
  
  // O código abaixo atualizará o valor de callMeScoped para "Eu sou do ClassB" para a única instância de ScopedImplementation 
  // E ele sobrescreverá o valor de callMeScoped para a instância de ClassA também. 
	ClassB.UpdateScopedFromClassB();
  
  // Agora, se a Classe A realizar qualquer operação em ScopedImplementation,
  // ela usará as propriedades/valores dos campos mais recentes que foram sobrescritos por ClassB.
	
  // O código abaixo atualizará o valor de callMeScoped para "Eu sou do ClassC"
  // E ele sobrescreverá o valor de callMeScoped para as instâncias de ClassA e ClassB também.
	ClassC.UpdateScopedFromClassC(); 
  // se Class B ou Class A realizar qualquer operação em ScopedImplementation, eles usarão as propriedades/valores dos campos mais recentes que foram sobrescritos por ClassC

    // outra implementação
}

Os serviços de alcance limitado são descartados no final de cada solicitação. Use Scoped quando você querer um comportamento estado-livre entre solicitações individuais.

Tempo de Trivia

O ciclo de vida de um serviço pode ser sobrescrito por um serviço pai onde ele é inicializado. Confuso? Deixe-me explicar:

Vamos usar o mesmo exemplo das classes acima e inicializar os serviços Transiente e Escopado a partir de SingletonImplementation (que é um singleton) como abaixo. Isso iniciaria os serviços ITransient e IScoped e sobrescreveria o ciclo de vida desses para o ciclo de vida de singleton como serviço pai. Neste caso, sua aplicação não teria nenhum serviço Transiente ou Escopado (considerando que você só tem esses 3 serviços que estávamos usando nos nossos exemplos). 

Leia as linhas no código abaixo:

C#

 

public class SingletonImplementation: ISingleton
{
	// construtor para adicionar a inicialização dos serviços.
	private readonly ITransient _transient 
	private readonly IScoped _scoped 
		
	SingletonImplementation(ITransient transient, IScoped scoped)
	{
		_transient = transient;  
        // Agora _transient se comportará como serviço de singleton, independentemente de como foi registrado como Transiente
		_scoped = scoped;
        // agora scoped se comportará como serviço de singleton, independentemente de ter sido registrado como Escopado
	}
    var callMeSingleton = ""
		
	// outra implementação
}

Resumo

Espero que o artigo acima seja útil para compreender o tópico. Recomendo que você mesmo experimente com o contexto definido acima e nunca mais seará confuso. Singleton é o mais fácil de entender pois, uma vez criada sua instância, ela será compartilhada pelas aplicações durante o ciclo de vida da aplicação. Nas mesmas linhas do Singleton, as instâncias Escopadas imitam o mesmo comportamento, mas apenas ao longo do ciclo de vida de uma solicitação em toda a aplicação. Transiente é totalmente estado menos, para cada solicitação e cada instância de classe manterá sua própria instância do serviço.

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