ASP.NET CORE

Inyección de Dependencias en ASP.NET Core

La Inyección de Dependencias o Dependency Injection (DI) es un conjunto de principios y patrones de diseño de software, que permiten crear código flexible o débilmente acoplado, simplificar el uso de pruebas unitarias y, en general, desarrollar un código limpio y fácil de mantener en el tiempo.

El acoplamiento es un concepto importante en la programación orientada a objetos. Se refiere a cómo una clase determinada depende de otras para realizar su función. El código débilmente acoplado no necesita conocer muchos detalles sobre un componente en particular para poder usarlo.

ASP.NET Core ha sido diseñado desde cero para ser modular y cumplir con las buenas prácticas de ingeniería de software. Tanto así, que la inyección de dependencias está integrada en el corazón del Framework, y es usada tanto internamente como en las aplicaciones que los desarrolladores crean. Independientemente si desea utilizar DI dentro de su propio código, las propias bibliotecas del Framework dependen de este patrón como concepto.

Propósito Subir

La Inyección de Dependencias no es el objetivo en sí, mas bien es un medio para un fin, el cual es escribir código que funcione de la manera más eficiente posible y sea fácil de mantener. Una excelente manera de hacer que el código sea más fácil de mantener es mediante un acoplamiento flexible.

¡Importante!
¡IMPORTANTE!

El libro Design Patterns, publicado en 1994 definía ya los beneficios de usar interfaces.

Manipular objetos únicamente en términos de la interfaz definida por clases abstractas reduce tanto las dependencias de implementación entre los subsistemas, que conduce al siguiente principio de diseño orientado a objetos reutilizable: "Programa una interfaz, no una implementación"

El acoplamiento flexible hace que el código sea extensible y la extensibilidad lo hace mantenible. DI no es más que una técnica que permite un acoplamiento flexible.

Antes de hablar sobre la inyección de dependencias y el uso de contenedores de inyección de dependencias, debemos comprender ¿Qué es una dependencia?

Dependencia Subir

Una dependencia es un objeto que depende otro objeto, o es algo que necesitamos para realizar una tarea, por ejemplo, una biblioteca, un paquete o una clase.

Así como los humanos dependemos de la comida, el agua, el sueño, etc. De manera similar, algunas de nuestras clases necesitan otras clases para funcionar correctamente o llevar a cabo una tarea especifica.

La siguiente Razor Page Register, muestra el uso de dependencias:

Register.razor
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

public class RegisterModel : PageModel
{
    public RegisterModel() { }

    public ContentResult OnGet(string username)
    {
        UserManager userManager = new();
        EmailSender emailSender = new();

        userManager.Create(username);
        emailSender.Send(username);

        return Content($"¡El usuario '{username}' fue registrado satisfactoriamente!");
    }
}

En el código C# anterior:

Dependencia uno, clase UserManager:

UserManager.cs
using System;

public class UserManager
{
    public void Create(string username)
    {
        Console.WriteLine($"Usuario '{username}' fue creado.");
    }
}

En el código C# anterior:

Dependencia dos, clase EmailSender:

EmailSender.cs
using System;

public class EmailSender
{
    public void Send(string email)
    {
        Console.WriteLine($"Correo electrónico de confirmación fue enviado a '{email}'!");
    }
}

En el código C# anterior:

¡Importante!
INFORMACIÓN

En términos generales, cada vez que usamos la palabra clave new, introducimos una nueva dependencia en nuestro código, lo que lo hace más difícil de probar y más resistente al cambio. Sin embargo, esto no significa que ya no debamos utilizar esa dependencia. Simplemente, debemos implementarlas de una forma eficiente.

Tipos de Dependencia

De forma general, los principales tipos de dependencia son:

Dependencias Visibles

Las dependencias visibles o explícitas son las que definen por medio del constructor, todo lo que la clase necesita para funcionar correctamente.

Register.razor
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

public class RegisterModel : PageModel
{
    private UserManager _userManager;
    private EmailSender _emailSender;

    public RegisterModel(UserManager userManager,
                         EmailSender emailSender)
    {
        _userManager = userManager;
        _emailSender = emailSender;
    }

    public ContentResult OnGet(string username)
    {
        _userManager.Create(username);
        _emailSender.Send(username);

        return Content($"¡El usuario '{username}' fue registrado satisfactoriamente!");
    }
}

En el código C# anterior:

Dependencias Ocultas

Las dependencias ocultas o implícitas son las que se definen por medio de campos (privados). Deben ser instanciadas manualmente dentro de la clase por medio de la palabra clave new.

Register.razor
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

public class RegisterModel : PageModel
{
    private UserManager _userManager;
    private EmailSender _emailSender;

    public RegisterModel()
    {
        _userManager = new();
        _emailSender = new();
    }

    public ContentResult OnGet(string username)
    {
        _userManager.Create(username);
        _emailSender.Send(username);

        return Content($"¡El usuario '{username}' fue registrado satisfactoriamente!");
    }
}

En el código C# anterior:

Otra forma de usar dependencias ocultas es la siguiente:

Register.razor
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

public class RegisterModel : PageModel
{
    private UserManager _userManager = new();
    private EmailSender _emailSender = new();

    public RegisterModel()
    {
    }

    public ContentResult OnGet(string username)
    {
        _userManager.Create(username);
        _emailSender.Send(username);

        return Content($"¡El usuario '{username}' fue registrado satisfactoriamente!");
    }
}

En el código C# anterior:

¡Importante!
¡IMPORTANTE!

En términos generales, toda dependencia debe ser explícita, no implícita.

El uso de dependencias implícitas hace que el código no sólo este cerrado a cambios y extensiones, sino que también complica probarlas, además de que viola el Principio de Responsabilidad Única o The Single Responsibility Principle (SRP) al hacer más de lo que debería. Es aconsejable evitarlas siempre que se pueda.

Implementación

El patrón de inyección de dependencias promueve el uso de interfaces e implementaciones. A continuación, se muestra la forma correcta de implementarlo:

Interface IUserManager y su clase de implementación UserManager:

IUserManager.cs y UserManager.cs
using System;

public interface IUserManager
{
    void Create(string username);
}

public class UserManager : IUserManager
{
    public void Create(string username)
    {
        Console.WriteLine($"Usuario '{username}' fue creado.");
    }
}

Interface IEmailSender y su clase de implementación EmailSender:

IEmailSender.cs y EmailSender.cs
using System;

public interface IEmailSender
{
    void Send(string email);
}

public class EmailSender : IEmailSender
{
    public void Send(string email)
    {
        Console.WriteLine($"Correo electrónico de confirmación fue enviado a {email}!");
    }
}

Razor Page Register:

Register.razor
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

public class RegisterModel : PageModel
{
    private readonly IUserManager _userManager;
    private readonly IEmailSender _emailSender;

    public RegisterModel(IUserManager userManager,
                         IEmailSender emailSender)
    {
        _userManager = userManager;
        _emailSender = emailSender;
    }

    public ContentResult OnGet(string username)
    {
        _userManager.Create(username);
        _emailSender.Send(username);

        return Content($"¡El usuario '{username}' fue registrado satisfactoriamente!");
    }
}

En el código C# anterior:

La mejor manera de hacer que su código sea más fácil de mantener y extender, es definir las dependencias por medio de abstracciones y utilizar la inyección de dependencias para resolverlas. Para llevar a cabo este proceso es necesario contar con un "lugar" donde registrarlas, y es de esto que trataremos a continuación.

Contenedor de Inyección de Dependencias Subir

Como se mencionó anteriormente, la inyección de dependencias es una técnica que permite implementar la Inversión de Control (IoC), puesto que la creación de las dependencias ya no es responsabilidad del desarrollador, sino de "un sistema" que se encargará de crear y mantenerlas “vivas” en el tiempo.

Este "lugar" o "sistema" donde se realizan las relaciones o asignaciones entre las abstracciones (interfaces) y sus implementaciones (clases) se denomina Contenedor de Inyección de Dependencias, o Contenedor de Inversión de Control (IoC) o simplemente Contenedor de Servicios.

El contenedor de servicios tiene como único objetivo y responsabilidad solventar el problema de crear, construir, resolver o instanciar servicios, objetos o dependencias, invirtiendo así la cadena de dependencias. En lugar de que el objeto raíz o llamador cree sus dependencias manualmente, el contenedor de servicios inspecciona los constructores de los servicios registrados, y proporciona una instancia ya creada y lista para usar.

Servicios y Componentes
SERVICIOS Y COMPONENTES

En Inyección de dependencias comúnmente se usan los términos Servicios y Componentes. Un servicio es un objeto que como su nombre lo indica, proporciona un servicio a otros objetos. Suele ser la abstracción o el contrato de lo que este ofrece. La implementación del servicio se denomina Componente, o en otras palabras, es la clase que implementa la interfaz o la que contiene el comportamiento.

ASP.NET Core proporciona un contenedor de servicios integrado, el cual asume la responsabilidad de crear instancias de todas las dependencias, y desecharlas cuando ya no son necesarias. Los servicios se registran mediante métodos de extensión definidos por la interfaz IServiceCollection, cuya implementación se obtiene mediante la propiedad WebApplicationBuilder.Services. Por lo general, los servicios se registran en el archivo Program.cs de la aplicación.

Cabe recalcar que el generador de aplicaciones web y servicios WebApplicationBuilder registra automáticamente la mayoría de los servicios internos necesarios para que la aplicación funcione correctamente, dependiendo su tipo. Por ejemplo, si creamos un proyecto Razor Pages, el Framework expone el método de extensión AddRazorPages, el cual registra o añade todos los servicios necesarios a la colección IServiceCollection.

Por convención, cada biblioteca que agrupa el registro de servicios debe exponer un método de extensión Add*(), el mismo que puede ser añadido a WebApplicationBuilder.Services.

El siguiente código, muestra el registro de las dependencias en el contenedor de servicios que ASP.NET Core proporciona.

Program.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
builder.Services.AddScoped<IUserManager, UserManager>();
builder.Services.AddScoped<IEmailSender, EmailSender>();

var app = builder.Build();

//Se omitió código por razón de brevedad.

app.Run();

En el código C# anterior:

Cabe recalcar que algunos métodos de extensión Add*() permiten especificar opciones adicionales, por lo general mediante una expresión lambda. Estas opciones permiten configurar o personalizar el registro del servicio en el contenedor.

Considere el siguiente código que permite controlar el acceso de las Razor Pages, mediante la configuración de convenciones de autorización a través del método de extensión AddRazorPages:

Program.cs
builder.Services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/Contact");
    options.Conventions.AuthorizeFolder("/Private");
    options.Conventions.AllowAnonymousToPage("/Private/PublicPage");
    options.Conventions.AllowAnonymousToFolder("/Private/PublicPages");
});

En el código C# anterior:

De forma general, todos los servicios que la aplicación requerirá deben estar documentados, ya que no hay forma exacta de conocerlo con anticipación o al compilar la aplicación. Si por algún motivo no se registra alguna dependencia en el contenedor, este no podrá resolverla y se producirá una excepción en tiempo de ejecución de tipo System.InvalidOperationException:

System.InvalidOperationException
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
An unhandled exception has occurred while executing the request.
System.InvalidOperationException: Unable to resolve service for type 'IUserManager' while attempting to activate 'Pages.RegisterModel'.
   at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.ThrowHelperUnableToResolveService(Type type, Type requiredBy)
   at lambda_method5(Closure, IServiceProvider, Object[])
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.DefaultPageModelFactoryProvider.<>c__DisplayClass3_0.<CreateModelFactory>b__0(PageContext pageContext)
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.CreateInstance()
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeInnerFilterAsync()
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
Consejo
CONSEJO

Al utilizar interfaces en lugar de tipos concretos, nos aseguramos de que podemos tener más de una implementación de la misma interfaz en un momento dado, y tal vez con ciclos de vida diferentes. Por ejemplo, si tenemos un código específico para el acceso a la base de datos PostgreSQL y mañana decidimos cambiarlo por una de SQL, no provocaremos una cascada de cambios en el código, ya que lo que está escrito en la interfaz es el contrato público, y ese es el punto común para dos tipos cualquiera que implementen la misma interfaz. Simplemente configuraremos al contenedor de servicios y el cambio se replicará en toda la aplicación.

El contenedor de servicios integrado de ASP.NET Core está diseñado para atender las necesidades del Framework y de la mayoría de las aplicaciones. Sin embargo, es posible implementar contenedores de terceros tales como:

Gráfico de Dependencias

Cuando usamos dependencias de forma encadenada, es decir, el hecho de que una clase dependa de otra, y que, a su vez, la segunda pueda también depender de otra, crea algo que se llama árbol de dependencias, gráfico de dependencias o gráfico de objetos, que no es otra cosa que el conjunto colectivo de dependencias que deben resolverse.

Mediante el gráfico de dependencias, el contenedor de servicios sabe que instancias debe pasar al objeto raíz o llamador (en este caso la Razor Page Register) puesto que en el mismo se definen sus dependencias, así como también cada clase de la que dependen sus dependencias, conformando de esta manera una cadena, árbol o estructura jerárquica.

En los ejemplos anteriores, por motivo de brevedad se obvió el código que crea un usuario y envía un correo electrónico. Sin embargo en la vida real, estos objetos dependen a su vez de otros, como por ejemplo: MailAddress, MailMessage, SmtpClient, NetworkCredential, ApplicationUser, SignInManager, etc.

¡Importante!
¡IMPORTANTE!

En el día a día es común encontrar cadenas de dependencias complejas, es decir clases que dependan de otras y que, a su vez, sus dependencias también dependan de otras más. Si el código no tiene este tipo de árbol de dependencias, es probable que las clases implementadas son demasiado grandes, por consiguiente, se estaría violando El Principio de Responsabilidad Única o The Single Responsibility Principle (SRP), ya que las mismas hacen más de lo que debería.

Vida Útil de los Servicios Subir

La vida útil de un servicio define cuánto durará su instancia, o qué tan actualizado debe estar. Por ejemplo, ¿hay que crear el servicio cada vez que se necesita? O ¿hay que crear uno nuevo por cada solicitud? o ¿hay que usar la misma instancia en toda la aplicación? La vida útil que utiliza para cada servicio puede afectar el comportamiento de la aplicación, por lo que es muy importante comprender cómo funciona.

Los servicios se pueden registrar con una de las duraciones siguientes:

Definición
DEFINICIÓN

La vida útil de un servicio define cuánto tiempo y cómo un contenedor de servicios debe utilizar un objeto determinado para cumplir una dependencia concreta. El hecho de que el contenedor cree una nueva instancia o reutilice una existente, depende de la vida útil utilizada para registrar el servicio.

En el siguiente artículo se tratará sobre la vida útil de los servicios en detalle.

Patrón Localizador de Servicios Subir

ASP.NET Core aconseja evitar el uso del patrón localizador de servicios, el cual consiste en acceder a un servicio fuera del contexto de su solicitud. En este caso, IServiceProvider actúa como un localizador de servicios, por lo que puede solicitar servicios directamente utilizando GetService() y GetRequiredService():

Es preferible usar GetRequiredService sobre GetService, ya que se puede conocer inmediatamente si existe algún problema con el servicio requerido mediante la generación de la excepción, además de evitar tratar con valores nulos.

¡Advertencia!
¡ADVERTENCIA!

El patrón Localizador de Servicios o Service Locator se considera un Anti-patrón, y se desaconseja su uso, puesto que viola los principios SOLID, y la encapsulación. Tal como se puede observar en los siguientes artículos de Mark Seemann: Service Locator is an Anti-Pattern, Service Locator violates SOLID, Service Locator violates encapsulation

Artículos Relacionados

Recursos Adicionales