Los desarrolladores pueden estar al tanto del ciclo de vida de las instancias de servicio al utilizar la inyección de dependencias, pero muchos no comprenden completamente cómo funciona. Pueden encontrar numerosos artículos en línea que explican estos conceptos, pero a menudo sólo reiteran definiciones que ya pueden conocer. Permítanme ilustrar con un ejemplo detallado que simplifica la explicación.
Al implementar la inyección de dependencias, los desarrolladores tienen tres opciones que determinan el ciclo de vida de las instancias:
- Singleton
- Scoped
- Transient
Mientras que la mayoría de los desarrolladores reconoce estos términos, un número significativo tiene dificultades para decidir qué opción elegir para el tiempo de vida de un servicio.
Definiciones
Permítanme comenzar con las definiciones:
- Singleton las instancias de servicio con vida única son creadas una vez por aplicación desde el contenedor de servicios. Una única instancia servirá a todas las solicitudes subsiguientes. Los servicios Singleton son eliminados al final de la aplicación (es decir, al reiniciar la aplicación).
- Transient las instancias de servicio con vida única son creadas por solicitud desde el contenedor de servicios. Los servicios Transient son eliminados al final de la solicitud.
- Scoped las instancias de servicio con vida única son creadas una vez por solicitud de cliente. Los servicios Transient son eliminados al final de la solicitud.
Cuándo Utilizar
- Singleton – Cuando quieres utilizar solo una instancia de un servicio a lo largo de toda la vida de la aplicación
- Transient – Cuando quieres utilizar instancias individuales de servicios dentro de la solicitud del cliente
- Scoped – Cuando quieres utilizar una sola instancia de servicio por cada solicitud
¿Qué es una solicitud del cliente? En palabras muy simples, puedes considerarla como una API/REST que llega a tu aplicación mediante los clics del usuario en los botones para obtener la respuesta.
No te preocupes, vamos a entender con un ejemplo.
Ejemplo
Antes veamos cómo crear interfaces/servicios y clases:
// declaramos 3 servicios de abajo
Public interface ISingleton
Public interface ITransient
Public interface IScoped
Ahora veamos la implementación de cada interfaz de servicio que creamos arriba. Vamos a intentar comprender el concepto intentando actualizar las variables callMeSingleton
, callMeTransient
, y callMeScoped
.
- Implementación de la clase Singleton:
class SingletonImplementation: ISingleton
{
var callMeSingleton = ""
// otra implementación
public SetSingleton(string value)
{
callMeSingleton = value;
}
// otra implementación
}
- Implementación de la clase Transient:
class TransientImplementation: ITransient
{
var callMeTransient = ""
// otra implementación
public SetTransient(string value)
{
callMeTransient = value;
}
// otra implementación
}
- Implementación de la clase Scoped:
class ScopedImplementation: IScoped
{
var callMeScoped = ""
// otra implementación
public SetScoped(string value)
{
callMeScoped = value;
}
// otra implementación
}
Ahora registremos (ConfigureServices
) con DI (Inyección de Dependencias) para decidir el ciclo de vida de cada instancia de servicio:
services.AddSingleton<ISingleton, SingletonImplementation>();
services.AddTransient<ITransient , TransientImplementation>();
services.AddScoped<IScoped , ScopedImplementation>();
Vamos a usar/llamar estos servicios desde 3 clases distintas (ClassA
, ClassB
y ClassC
) para entender el ciclo de vida de cada servicio:
ClassA
:
public class ClassA
{
private ISingleton _singleton;
//constructor para instanciar 3 servicios diferentes que creamos
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");
}
// otra implementación
}
ClassB
:
public class ClassB
{
private ISingleton _singleton;
//constructor para instanciar 3 servicios diferentes que creamos
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");
}
// otra implementación
}
ClassC
:
public class ClassC
{
private ISingleton _singleton;
//constructor para instanciar 3 servicios diferentes que creamos
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");
}
// otra implementación
}
Análisis
Vamos a analizar los resultados y el comportamiento para cada ciclo de vida uno por uno a partir de la implementación anterior:
Singleton
Todas las clases (ClassA
, ClassB
y ClassC
) utilizarán la misma instancia única de la clase SingletonImplementation
a lo largo del ciclo de vida de la aplicación. Esto significa que las propiedades, campos y operaciones de la clase SingletonImplementation
serán compartidos entre las instancias utilizadas en todas las clases llamantes. Cualquier actualización a las propiedades o campos anularán cambios previos.
Por ejemplo, en el código de arriba, ClassA
, ClassB
, y ClassC
están todos utilizando el servicio SingletonImplementation
como una instancia de singleton y llamando a SetSingleton
para actualizar la variable callMeSingleton
. En este caso, habrá un solo valor para la variable callMeSingleton
para todas las solicitudes que intentan acceder a esta propiedad. Cualquier clase que lo acceda última para actualizarsoverride el valor de callMeSingleton
.
ClassA
– tendrá la misma instancia que las otras clases para el servicioTransientImplementation
.ClassB
– tendrá la misma instancia que las otras clases para el servicioTransientImplementation
.ClassC
– tendrá la misma instancia que las otras clases para el servicioTransientImplementation
.
ClassA
, ClassB
, y ClassC
están actualizando la misma instancia de la clase SingletonImplementation
, lo que sobreescribirá el valor de callMeSingleton
. Por lo tanto, tenga cuidado al establecer o actualizar propiedades en la implementación del servicio singleton.
Los servicios singleton se desechan al final de la aplicación (es decir, al reiniciar la aplicación).
Transient
Todas las clases (ClassA
, ClassB
, y ClassC
) utilizarán sus propias instancias individuales de la clase TransientImplementation
. Esto significa que si una clase llama a propiedades, campos o operaciones de la clase TransientImplementation
, solo actualizará o sobreescribirá sus valores de instancia individuales. Las actualizaciones de las propiedades o campos no se comparten entre otras instancias de TransientImplementation
.
Vamos a entender:
ClassA
– tendrá su propia instancia del servicio deTransientImplementation
.ClassB
– tendrá su propia instancia del servicio deTransientImplementation
.ClassC
– tendrá su propia instancia del servicio deTransientImplementation
.
Supongamos que tienes una ClassD
que está llamando al servicio transitorio desde las instancias de ClassA
, ClassB
y ClassC
. En este caso, cada instancia de clase sería tratada como una instancia diferente/separada y cada clase tendría su propio valor de callMeTransient
. Lee los comentarios en línea abajo para ClassD
:
public ClassD
{
// Otra implementación
// La línea de código de abajo actualizará el valor de callMeTransient a "Yo soy de la ClaseA" solo para la instancia de la ClaseA.
// Y no será cambiado por ninguna próxima llamada de la Clase B o de la Clase B
ClassA.UpdateTransientFromClassA();
// La línea de código de abajo actualizará el valor de callMeTransient a "Yo soy de la ClaseB" solo para la instancia de la ClaseB.
// Y no sobreescribirá el valor para la instancia de la ClaseA ni será cambiado por la próxima llamada de la Clase C
ClassB.UpdateTransientFromClassB();
// La línea de código de abajo actualizará el valor de callMeTransient a "Yo soy de la ClaseC" solo para la instancia de la ClaseC.
// Y no sobreescribirá el valor para las instancias de ClaseA y ClaseB ni será cambiado por ninguna próxima llamada de otra clase.
ClassC.UpdateTransientFromClassC();
// Otra implementación
}
Los servicios Transient se deshacen al final de cada solicitud. Use Transient cuando desea un comportamiento sin estado dentro de la solicitud.
Scoped
Todas las clases (ClassA
, ClassB
, y ClassC
) usarán una sola instancia de la clase ScopedImplementation
por cada solicitud. Esto significa que las llamadas a las propiedades/campos/operaciones de la clase ScopedImplementation
ocurrirán en una sola instancia dentro del scope de la solicitud. Cualquier actualización de las propiedades/campos será compartida entre otras clases.
Vamos a entender:
ClassA
– Tendrá su propia instancia del servicio deTransientImplementation
.ClaseB
– tendrá la misma instancia del servicio deTransientImplementation
queClaseA
.ClaseC
– tendrá la misma instancia del servicio deTransientImplementation
queClaseA
yClaseB
.
Supongamos que tienes una ClaseD
que está llamando a un servicio de ámbito desde las instancias de ClaseA
, ClaseB
y ClaseC
. En este caso, cada clase tendrá una sola instancia de la clase ScopedImplementation
. Lee los comentarios en línea para ClaseD
abajo.
public class ClassD
{
// otra implementación
// De abajo en adelante, el código actualizará el valor de callMeScoped a "Yo soy de la ClaseA" para la instancia de ClaseA
// Sin embargo, como es un ciclo de vida Scoped, mantiene una sola instancia de ImplementaciónScoped
// Entonces, puede ser anulada por la siguiente llamada de ClaseB o ClaseC
ClassA.UpdateScopedFromClassA();
// De abajo en adelante, el código actualizará el valor de callMeScoped a "Yo soy de la ClaseB" para la sola instancia de ImplementaciónScoped
// Y también anulará el valor de callMeScoped para la instancia de ClaseA.
ClassB.UpdateScopedFromClassB();
// Ahora, si Clase A realizará alguna operación en ImplementaciónScoped,
// utilizará las propiedades/valores de campo más recientes que han sido anulados por ClaseB.
// De abajo en adelante, el código actualizará el valor de callMeScoped a "Yo soy de la ClaseC"
// Y también anulará el valor de callMeScoped para las instancias de ClaseA y ClaseB.
ClassC.UpdateScopedFromClassC();
// Si Clase B o Clase A realizarán alguna operación en ImplementaciónScoped, utilizarán los valores de propiedad/campo más recientes que han sido anulados por ClaseC
// otra implementación
}
Los servicios con ciclo de vida Scoped son desechados al final de cada solicitud. Utilice Scoped cuando desee un comportamiento sin estado entre solicitudes individuales.
Hora de trivia
El ciclo de vida de un servicio puede ser anulado por un servicio padre en el que se inicializa. ¿Te confundes? Permíteme explicar:
Vamos a tomar el mismo ejemplo de las clases anteriores y inicializar los servicios Transient y Scoped desde SingletonImplementation
(que es un singleton) como se muestra a continuación. Esto iniciaría los servicios ITransient
y IScoped
y sobreescribiría el ciclo de vida de estos para que sean del ciclo de vida de singleton como servicio padre. En este caso, tu aplicación no tendría ningún servicio Transient o Scoped (teniendo en cuenta que solo tienes estos 3 servicios que utilizamos en nuestros ejemplos).
Revise las líneas en el código de abajo:
public class SingletonImplementation: ISingleton
{
// constructor para agregar la inicialización de los servicios.
private readonly ITransient _transient
private readonly IScoped _scoped
SingletonImplementation(ITransient transient, IScoped scoped)
{
_transient = transient;
// Ahora _transient se comportaría como un servicio singleton sin importar cómo fue registrado como Transient
_scoped = scoped;
// ahora scoped se comportaría como singleton sin importar que está registrado como Scoped
}
var callMeSingleton = ""
// otra implementación
}
Resumen
Espero que el artículo anterior sea de ayuda para entender el tema. Recomendaría que pruebe esto con el contexto establecido anteriormente y nunca más estará confundido. Singleton es el más fácil de entender porque una vez que crees su instancia, se compartirá a través de las aplicaciones a lo largo de todo el ciclo de vida de la aplicación. Del mismo modo que el Singleton, las instancias Scoped imitan el mismo comportamiento pero solo a lo largo del ciclo de vida de una solicitud a lo largo de la aplicación. Transient es totalmente estado-menos, para cada solicitud y cada instancia de clase tendrá su propia instancia de servicio.
Source:
https://dzone.com/articles/understanding-the-dependency-injection-lifecycle