aspnet-core-inyeccion-dependencias-ciclos-vida

Inyección de dependencias en ASP.NET Core

  • 4 min

La Inyección de Dependencias es un patrón para recibir objetos en vez de crearlos dentro de la clase.

En el artículo anterior vimos qué son los servicios y por qué nos ayudan a limpiar el código. Pero nos quedamos con un problema: seguíamos haciendo new MiServicio() manualmente.

Vamos a solucionar eso usando una de las herramientas más importantes de ASP.NET Core: el contenedor de inyección de dependencias.

Qué problema resuelve

Sin inyección de dependencias, una clase crea todo lo que necesita:

public class UserService
{
    private readonly EmailService _emailService = new EmailService();
}
Copied!

Esto parece cómodo al principio, pero crea acoplamiento fuerte. UserService queda pegado a EmailService, y cambiarlo por otra implementación o probarlo se vuelve más incómodo.

Con inyección de dependencias, la clase pide lo que necesita:

public class UserService
{
    private readonly IEmailService _emailService;

    public UserService(IEmailService emailService)
    {
        _emailService = emailService;
    }
}
Copied!

Ahora UserService no sabe quién crea IEmailService. Solo sabe que alguien se lo entrega ya preparado.

El contenedor de servicios

En ASP.NET, registramos los servicios en Program.cs, antes de llamar a builder.Build().

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IEmailService, EmailService>();

var app = builder.Build();
Copied!

Con ese registro le estamos diciendo a .NET:

Cuando alguien pida IEmailService, entrégale un EmailService.

Y ya está. No hay más ceremonia.

Inyectar un servicio

En Minimal APIs podemos pedir el servicio como parámetro del endpoint:

app.MapPost("/registro", (
    UsuarioDto user,
    IUserService userService) =>
{
    userService.Register(user);
    return Results.Ok();
});
Copied!

En controladores, lo habitual es inyectarlo en el constructor:

[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
    private readonly IUserService _userService;

    public UsersController(IUserService userService)
    {
        _userService = userService;
    }

    [HttpPost]
    public IActionResult Create(UsuarioDto user)
    {
        _userService.Register(user);
        return Ok();
    }
}
Copied!

El controlador queda centrado en HTTP, y el servicio se encarga de la lógica.

Ciclos de vida

Al registrar un servicio, tenemos que elegir cuánto tiempo vive la instancia creada.

MétodoInstancia creadaUso típico
AddTransientCada vez que se pideUtilidades ligeras y sin estado
AddScopedUna vez por petición HTTPServicios de negocio, repositorios, DbContext
AddSingletonUna vez por aplicaciónCachés, configuración, servicios compartidos seguros

AddTransient() crea una instancia nueva cada vez que alguien pide el servicio.

builder.Services.AddTransient<ITextFormatter, TextFormatter>();
Copied!

Es útil para servicios pequeños, sin estado y baratos de crear.

AddScoped() crea una instancia por petición HTTP.

builder.Services.AddScoped<IUserService, UserService>();
Copied!

Es el ciclo de vida más habitual en aplicaciones web. Durante una misma petición se reutiliza la misma instancia, pero otra petición tendrá la suya.

Por eso encaja tan bien con DbContext: queremos compartirlo dentro de una operación, pero no mezclar datos entre usuarios distintos.

AddSingleton() crea una sola instancia para toda la aplicación.

builder.Services.AddSingleton<ICacheService, CacheService>();
Copied!

Va bien para objetos compartidos y seguros para concurrencia. Pero hay que usarlo con cuidado.

No guardes estado mutable de usuario dentro de un singleton. Todos los usuarios comparten la misma instancia, así que una variable interna puede convertirse en una fiesta bastante incómoda.

También hay una regla importante: no debemos inyectar servicios Scoped dentro de servicios Singleton. Un singleton vive durante toda la aplicación, mientras que un scoped pertenece a una petición concreta.

Interfaces e implementaciones

Lo normal es registrar una interfaz y una implementación:

builder.Services.AddScoped<IUserService, UserService>();
Copied!

Así el código consume IUserService, no UserService directamente.

Esto nos permite cambiar la implementación sin tocar el código consumidor. Por ejemplo, en pruebas podemos usar un FakeUserService o un mock.

builder.Services.AddScoped<IUserService, FakeUserService>();
Copied!

Esa separación es una de las bases para hacer pruebas unitarias sin tener que arrancar media aplicación.