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:
- 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 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:
// 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:
class SingletonImplementation: ISingleton
{
var callMeSingleton = ""
// outra implementação
public SetSingleton(string value)
{
callMeSingleton = value;
}
// outra implementação
}
- Implementação da classe Transient:
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 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>();
`
Usemos estes 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");
}
// outras implementações
}
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");
}
// outras implementações
}
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");
}
// 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çoTransientImplementation
.ClassB
– terá a mesma instância dos outros classes para o serviçoTransientImplementation
.ClassC
– terá a mesma instância dos outros 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 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 deTransientImplementation
.ClassB
– Ela terá sua própria instância do serviço deTransientImplementation
.ClassC
– Ela terá sua própria instância do serviço deTransientImplementation
.
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
:
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 deTransientImplementation
.ClassB
– Terá a mesma instância de serviço deTransientImplementation
queClassA
.ClassC
– Terá a mesma instância de serviço deTransientImplementation
queClassA
eClassB
.
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.
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:
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