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 compreendem totalmente como ela funciona. Você pode encontrar numerosos artigos online que esclarecem esses conceitos, mas eles frequentemente 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:
- Singleton
- Scoped
- 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 serviço com tempo de vida são criadas uma vez por aplicação a partir do container de serviços. Uma instância única servirá todas as solicitações subsequentes. Serviços Singleton são descartados no final da aplicação (isto é, na reinicialização da aplicação).
- Transient instâncias de serviço com tempo de vida são criadas por solicitação a partir do container de serviços. Serviços Transient são descartados no final da solicitação.
- Scoped instâncias de serviço com tempo de vida são criadas uma vez por solicitação de cliente. Serviços Transient são descartados no final da solicitação.
Quando Usar
- Singleton – Quando você quer usar instâncias únicas de serviços ao longo do ciclo de vida da aplicação
- Transiente – Quando você quer usar instâncias individuais de serviços dentro da solicitação do cliente
- Scoped – Quando você quer usar uma instância única de serviço para cada solicitação
O que é uma solicitação do cliente? Em palavras simples, você pode considerá-la como uma API/REST chamada 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:
// declaramos 3 serviços abaixo
Public interface ISingleton
Public interface ITransient
Public interface IScoped
Agora vamos escrever a implementação para cada interface de serviço/classe criada acima. Vamos tentar entender o conceito tentando atualizar a variável callMeSingleton
, callMeTransient
, e callMeScoped
.
- Implementação da classe Singleton:
class SingletonImplementation: ISingleton
{
var callMeSingleton = ""
// outra implementação
public SetSingleton(string value)
{
callMeSingleton = value;
}
// outra implementação
}
- Implementação da classe Transiente:
class TransientImplementation: ITransient
{
var callMeTransient = ""
// outra implementação
public SetTransient(string value)
{
callMeTransient = value;
}
// outra implementação
}
- Implementação da classe Scoped:
class ScopedImplementation: IScoped
{
var callMeScoped = ""
// outra implementação
public SetScoped(string value)
{
callMeScoped = value;
}
// outra implementação
}
Vamos registrar (ConfigureServices
) com o DI (Injeção de Dependência) para decidir o ciclo de vida de cada instância de serviço:
services.AddSingleton<ISingleton, SingletonImplementation>();
services.AddTransient<ITransient , TransientImplementation>();
services.AddScoped<IScoped , ScopedImplementation>();
Vamos usar/chamar esses serviços de 3 classes diferentes (ClassA
, ClassB
, e ClassC
) para entender o ciclo de vida de cada serviço:
ClassA
:
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");
}
//outra implementação
}
ClassB
:
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");
}
//outra implementação
}
ClassC
:
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");
}
//outra implementação
}
Análise
Vamos analisar os resultados e o comportamento para cada ciclo de vida individualmente a partir da implementação acima:
Singleton
Todas as classes (ClassA
, ClassB
, e ClassC
) usarão a mesma instância única do objeto SingletonImplementation
ao longo do ciclo de vida do aplicativo. Isso significa que as propriedades, campos e operações da classe SingletonImplementation
serão partilhadas entre as instâncias usadas em todas as classes chamadas. Qualquer atualização a propriedades ou campos substituirá mudanças 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 por último para atualizar substituirá o valor de callMeSingleton
.
ClassA
– Terá a mesma instância que as outras classes para o serviçoTransientImplementation
.ClassB
– Terá a mesma instância que as outras classes para o serviçoTransientImplementation
.ClassC
– Terá a mesma instância que as outras classes para o serviçoTransientImplementation
.
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 definindo ou atualizando propriedades na implementação do serviço singleton.
Serviços singleton são descartados no final do aplicativo (isto é, ao reiniciar o 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. Alterações em propriedades ou campos não são partilhadas entre outras instâncias de TransientImplementation
.
Vamos entender:
ClassA
– Tera sua própria instância do serviço deTransientImplementation
.ClassB
– Tera sua própria instância do serviço deTransientImplementation
.ClassC
– Tera sua própria instância do serviço deTransientImplementation
.
Digamos que você tem 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
:
public ClassD
{
// outra implementação
// A linha de código abaixo atualizará o valor de callMeTransient para "Sou da 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 atualizará o valor de callMeTransient para "Sou da 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 atualizará o valor de callMeTransient para "Sou da 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
}
Serviços transitórios são descartados no final de cada solicitação. Use Transient quando você quiser um comportamento stateless dentro da solicitação.
Scoped
Todas as classes (ClassA
, ClassB
e ClassC
) usarão instâncias únicas da classe 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 deTransientImplementation
.ClasseB
– Terá a mesma instância de serviço deTransientImplementation
queClasseA
.ClasseC
– Terá a mesma instância de serviço deTransientImplementation
queClasseA
eClasseB
.
Digamos que você tem uma ClasseD
que está chamando um serviço de alcance de instância de ScopedImplementation
de instâncias de ClasseA
, ClasseB
e ClasseC
. Neste caso, cada classe terá uma única instância de ScopedImplementation
da classe. Leia os comentários em linha para ClasseD
abaixo.
public class ClassD
{
// outra implementação
// O código abaixo atualizará o valor de callMeScoped para "Eu sou de ClassA" para a instância de ClassA
// Mas como tem uma vida útil scope, ele mantém apenas uma instância de ScopedImplementation
// Em seguida, pode ser sobrescrito por próxima chamada de ClassB ou ClassC
ClassA.UpdateScopedFromClassA();
// O código abaixo atualizará o valor de callMeScoped para "Eu sou de ClassB" para a única instância de ScopedImplementation
// E também sobrescreverá o valor de callMeScoped para a instância de ClassA.
ClassB.UpdateScopedFromClassB();
// Agora, se a Classe A realizar alguma operação em ScopedImplementation,
// ela usará as propriedades/campos mais recentes que foram sobrescritos por ClassB.
// O código abaixo atualizará o valor de callMeScoped para "Eu sou de ClassC"
// E também sobrescreverá o valor de callMeScoped para as instâncias de ClassA e ClassB.
ClassC.UpdateScopedFromClassC();
// se Class B ou Class A realizar alguma operação em ScopedImplementation, usará as propriedades/campos mais recentes que foram sobrescritos por ClassC
// outra implementação
}
Os serviços de escopo são descartados no final de cada solicitação. Use o Scoped quando você quiser um comportamento estado-menos entre solicitações individuais.
Tempo de Trivia
O ciclo de vida de um serviço pode ser sobrescrito por um serviço de pai onde ele é inicializado. Confuso? Deixe-me explicar:
Vamos pegar o mesmo exemplo das classes acima e inicializar os serviços Transient e Scoped a partir de SingletonImplementation
(que é um singleton) conforme mostrado abaixo. Isso iniciará os serviços ITransient
e IScoped
e sobrescreverá o ciclo de vida destes para o ciclo de vida do singleton como serviço pai. Neste caso, sua aplicação não teria nenhum serviço Transient ou Scoped (considerando que você tem apenas esses 3 serviços que usamos em nossos exemplos).
Leia as linhas no código abaixo:
public class SingletonImplementation: ISingleton
{
// construtor para adicionar inicializar os serviços.
private readonly ITransient _transient
private readonly IScoped _scoped
SingletonImplementation(ITransient transient, IScoped scoped)
{
_transient = transient;
// Agora _transient seria um serviço singleton mesmo que fosse registrado como Transient
_scoped = scoped;
// agora scoped seria um serviço singleton mesmo que fosse registrado como Scoped
}
var callMeSingleton = ""
// outra implementação
}
Summary
Espero que o artigo acima seja útil para entender o tópico. Eu recomendaria tentar isso você mesmo com o contexto acima e você nunca mais ficará confuso. Singleton é o mais fácil de entender porque assim que você criar sua instância, ela será compartilhada entre as aplicações ao longo do ciclo de vida da aplicação. Na linha doSingleton, as instâncias Scoped imitam o mesmo comportamento, mas apenas ao longo do ciclo de vida de uma solicitação na aplicação. Transient é totalmente estado-livre, para cada solicitação e para cada instância de classe, ela manterá sua própria instância de serviço.
Source:
https://dzone.com/articles/understanding-the-dependency-injection-lifecycle