Inyección de dependencias

En este post vamos a ver qué es la inyección de dependencias principalmente en un entorno web, aunque como ya vimos en el post de inversión de dependencias  también pueden ser ejecutadas en una aplicación de consola. 

Veremos por qué utilizar inyección de dependencias, los diferentes tipos u opciones que podemos utilizar al crear nuestro contenedor de dependencias tanto scoped, singleton como transient, 

Finalmente un Ejemplo en C#. 

 

Comúnmente a la inyección de dependencias se le denomina DI que viene de sus siglas en inglés de Dependency Injection

 

1 - Por qué utilizar inyección de dependencias

Debemos entender que la inyección de dependencias es un patrón de diseño; El uso de este patrón nos enlaza dos conceptos, el primero ya lo hemos visto, el principio SOLID de inversión de dependencias junto con Inversión of control.

 

ASP.NET Core ha sido pensado desde un principo para que desarrollemos nuestro código utilizando inyección de dependencias y todo el framework hace uso de ellas. 

Para comprender qué es la inyección de dependencias debemos entender que es una dependencia. 

 

1.1 - Qué es una dependencia

Siguiendo el ejemplo de este curso, tenemos una clase que recoge el perfil personal.

Tenemos un código que recoge la información ya bien sea de la base de datos o de un sistema de ficheros, la agrupa y la muestra.

La acción de coger de la base de datos, decimos que es una dependencia.

public PersonalProfileDto GetPersonalProfileDto(int userId)
{
    var personalProfileInfo = new PersonalProfileInfo();
    var pefil = personalProfileInfo.GetPerfil(userId);
    var skills = personalProfileInfo.GetSkills(userId);
    var intereses = personalProfileInfo.GetIntereses(userId);

    return Map(peril, skills, intereses);
}

Actualmente nuestro método GetPersonalProfileDto es el responsable de la creación de la instancia de la dependencia del perfil.  

 

1.2 - Problemas de instanciar las dependencias

Primero, como el método es el responsable de la creación de la instancia, este está ligado fuertemente a nuestra actual implementación. 

este problema siempre viene enlazado con cambios en el futuro, ya que si deseamos cambiar, por ejemplo, que recoja los datos de una API pública en vez de de la base de datos, deberemos cambiar también todas las implementaciones que tenemos de new PersonalProfileInfo();  a  new PersonalProfileInfoAPI();

Este ejemplo hace que nuestro código sea mucho más difícil de mantener. 

 

Otro Problema es cuando queremos hacer unit test nuestros métodos tienen lógica que queremos testear. Pero para testear este pequeño método deberíamos también implementar todo lo que la instancia de PersonalProfileInfo necesite y hacer que nos devuelva la información que necesitamos en los métodos que llamamos. 

Por este motivo testear cuando tenemos que instanciar dependencias es casi imposible testear correctamente.

 

2 - Uso de interfaces en Inyección de dependencias

El principio de inversión de dependencias nos indica que nuestras clases dependen de abstracciones y no de implementaciones.

 

Pero qué quiere decir, actualmente nuestro método está ligado a la implementación de PersonalProfileInfo, para abstraer nuestro código debemos extraer una interfaz y nuestra clase PersonalProfileInfo deberá implementar esta interfaz . 

 

La interfaz va a contener todos los métodos que necesitamos para construir el perfil personal.

 

public interface IPersonalProfileInfo
{
    List<InterestEntity> GetInterests(int userId);
    List<SkillEntity> GetSkills(int iduserId);
    PersonalProfileEntity GetPerfil(int userId);
}

public class PersonalProfileInfo : IPersonalProfileInfo
{
    //code
}

Gracias a esta implementación de la interfaz seremos capaces de crear test unitarios que contengan dependencias ya que podremos hacer un mock de las mismas

 

3 - inyección de dependencias por constructor

Nuestro cambio en el código no acaba aquí ya que nuestro método principal no lo hemos cambiado.

Para nuestro siguiente cambio vamos a tratar la inyección de dependencias por constructor o “constructor inyector” en inglés.

 

Debemos crear en nuestra clase o servicio que contiene el método GetPersonalProfileDto un constructor, y este constructor debe recibir por parámetro la interfaz que acabamos de crear.

 

El parámetro debemos asignarlo a una propiedad de la clase, la cual será inmutable y así evitamos que la propiedad pueda ser modificada. 

 

Finalmente en nuestro método GetPersonalProfileDto ya no utilizaremos la instancia de new PersonalProfileInfo() sino que utilizaremos la propiedad a la que acabamos de asignar el valor desde el constructor.

public class PerilPersonal
{
    private readonly IPersonalProfileInfo _personalProfileInfoDependency;

    public PerfilPersonal(IPersonalProfileInfo personalProfileInfo)
    {
        _personalProfileInfo = personalProfileInfo;
    }

    public PersonalProfileDto GetPersonalProfileDto(int userId)
    {
        var pefil = _personalProfileInfoDependency.GetPerfil(userId);
        var skills = _personalProfileInfoDependency.GetSkills(userId);
        var intereses = _personalProfileInfoDependency.GetIntereses(userId);

        return Map(peril, skills, intereses);
    }
}

 

4 - Registrar nuestras dependencias con el contenedor de dependencias

Para poder utilizar inyección de dependencias, debemos indicar a qué clase hace referencia cada una de nuestras interfaces que estamos inyectando.

Debemos configurar el contenedor de inyección de dependencias cuando la aplicación comienza su ejecución. 

En ASP.NET Core utilziamos IServiceCollection para registrar nuestras propios servicios (dependencias). Cualquier servicio que quieras inyectar debe estar registrado en el contenedor IServiceCollection y estos seran resulstos por el tipo IServiceProvider una vez nuestro IServiceCollection ha sido construido. 

Suena un poco raro, pero es muy sencillo, vamos al lío;

 

En ASP.NET Core la ubicación más común para registrar el contenedor es en el método ConfigureServices de la clase startup.

Para registrar nuestros servicios (dependencias) tenemos varios extension methods que nos permitiran hacerlo.

Para ello tenemos tres opciones, Scoped, Transient y Singleton, veremos sus diferencias en el siguiente punto.

 

Las tres opciones registran nuestro servicio en el contenedor de dependencias. 

Contienen la misma estructura ya que aceptan dos argumentos genéricos

services.AddTransient<IPersonalProfileInfo, PersonalProfileInfo>();
services.AddScoped<IPersonalProfileInfo, PersonalProfileInfo>();
services.AddSingleton<IPersonalProfileInfo, PersonalProfileInfo>();

 

  • El primer argumento es nuestra abstracción, osea la interfaz que vamos a utilizar a la hora de inyectar en los diferentes constructores.
  • El segundo elemento es la implementación que hace uso de la abstracción. 

 

En caso de que NO queramos introducir una abstracción podemos indicar directamente la clase.

services.AddTransient<PersonalProfileInfo>();

 

Tipos de inyección de dependencias 

Cuando registramos los servicios en nuestro contenedor tenemos 3 opciones como he explicado arriba, estas tres opciones sólo difieren en una sola acción, el tiempo de vida.

El contenedor mantiene todos los servicios que crea y una vez su tiempo de vida (lifetime) termina, son disposed o liberados para el garbage collector.

Es importante elegir el tipo de tiempo de vida correctamente, ya que no todos nos darán los mismos resultados.

 

Para este ejemplo tenemos un pequeño servicio que simplemente nos genera un GUID cada vez que es instanciado, junto con un pequeño controller y un middleware.

public class GuidService
{
    public readonly Guid ResultadoGuid;

    public GuidService()
    {
        ResultadoGuid = Guid.NewGuid();
    }
}
[ApiController]
[Route("[controller]")]
public class HomeController : ControllerBase
{
    private readonly GuidService _guidService;
    private readonly ILogger<HomeController> _logger;

    public HomeController(GuidService guidService, ILogger<HomeController> logger)
    {
        _guidService = guidService;
        _logger = logger;
    }

    [HttpGet]
    public IActionResult Index()
    {
        var logMessage = $"Controller:{_guidService.ResultadoGuid}";
        _logger.LogInformation(logMessage);
        return Ok();
    }
}

public class EjMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<EjMiddleware> _logger;
    public EjMiddleware(RequestDelegate next, ILogger<EjMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context, GuidService guidService)
    {
        var logMessage = $"Middleware: {guidService.ResultadoGuid}";
        _logger.LogInformation(logMessage);
        await _next(context);
    }
}

La aplicación se puede descargar desde GitHub

 

4.1 - Cuándo utilizar Transient

Cuando un servicio es registrado como transient quiere decir que se va a crear una instancia cada vez que se resuelva la dependencia. 

En otras palabras, cada vez que utilicemos un servicio “Transient” este será una nueva instancia:

transient

  • Utilizamos Transient cuando el servicio contiene estado que puede cambiar (es mutable)  y no es thread-safe. Ya que cada servicio va a recibir su propia instancia, podemos consumir los métodos sin preocuparnos por si otros servicios están accediendo.
  • Por contra debemos gastar más memoria ya que necesitaremos más objetos y será algo más ineficiente.
  • Es muy común utilizar servicios transient cuando no tenemos claro qué opción utilizar. 

Este sería el resultado de la ejecución del código de ejemplo

[
    "Middleware: 1b3051c7-1ea0-481d-8edf-18d3c9ef6496",
    "Controller: 03b111f2-62b7-4996-8cb6-bb83f782403b"
]

Como vemos, para cada una de las clases ha creado una instancia.

 

4.2 - Cuándo utilizar Scoped 

Si creamos un servicio que está registrado como scoped este vivirá por el tiempo de vida que exista ese scope.

En ASP.NET Core la aplicación crea un scope para cada request que recibe, normalmente desde el navegador.

Cada servicio scoped se creará una vez por request.

scoped

Lo que quiere decir, que todos los servicios recibirán la misma instancia mientras dure el scope (la request) :

Request 1:
[
    "Middleware: 0cff9660-1b62-4475-a1ae-976d9516abf9",
    "Controller: 0cff9660-1b62-4475-a1ae-976d9516abf9"
]

Request 2:
[
    "Middleware: 52dd3c3f-435e-4d72-99a3-00e227442ef4",
    "Controller: 52dd3c3f-435e-4d72-99a3-00e227442ef4"
]

Un ejemplo muy común de creación de scoped es cuando tenemos un "logId" para poder hacer un mapa de todos los puntos por los que la llamada ha pasado. 

 

4.3 - Cuándo utilizar Singleton

Cuando un servicio está registrado como singleton quiere decir que va  ser creado únicamente una única vez mientras la aplicación esté activa. 

La misma instancia será reutilizada e inyectada en todas las clases que la utilicen.  

singleton

Si el servicio se utiliza muy frecuentemente puede proveer mejora en el rendimiento ya que se instancia una vez para la aplicación entera y no uno por servicio como hemos visto en Transient. 

Si utilizamos singleton debemos crear la clase a la que referencia o bien completamente inmutable ya que al ser la misma instancia en caso de ser mutable podría dar errores inesperados.

O utilizar técnicas donde nos aseguramos que la aplicación va a ser thread-safe. Un ejemplo muy común de singleton es caché en memoria.

 

Request 1:
[
    "Middleware: 01ae4b92-18ab-457a-be01-9a47ac231915",
    "Controller: 01ae4b92-18ab-457a-be01-9a47ac231915"
]
Request 2:
[
    "Middleware: 01ae4b92-18ab-457a-be01-9a47ac231915",
    "Controller: 01ae4b92-18ab-457a-be01-9a47ac231915"
]

Finalmente utilizando singleton debemos tener cuidado ya que una única instancia puede suponer un gran montón de memoria, que nunca será liberada por el garbage collector, debemos tener esto muy en cuenta. 

 

4.4 - Validación del scope

Cuando programamos, definir que tipo de lifetime vamos a dar a nuestros servicios es importante, pero no solo eso, sino que debemos asegurarnos que nuestros servicios no dependen en otros servicios con tiempos de vida menores que ellos.

Y esto es debido a que así evitaremos errores o funcionamientos extraños durante el tiempo de ejecución.

Desde ASP.NET Core 2.0 el propio framework viene con una validación que comprobara que estamos añadiendo todos los scope de forma correcta. 

 

 

Conclusión

La inyección de dependencias nos permite crear aplicaciones que son mucho más fáciles de mantener y trabajar en ellas. 

Además debido a las abstracciones nos permite crear interfaces que nos ayudan a la hora de crear test. 

El código es mucho más limpio y legible.