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();
}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;
}
}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();Con ese registro le estamos diciendo a .NET:
Cuando alguien pida
IEmailService, entrégale unEmailService.
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();
});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();
}
}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étodo | Instancia creada | Uso típico |
|---|---|---|
AddTransient | Cada vez que se pide | Utilidades ligeras y sin estado |
AddScoped | Una vez por petición HTTP | Servicios de negocio, repositorios, DbContext |
AddSingleton | Una vez por aplicación | Cachés, configuración, servicios compartidos seguros |
AddTransient() crea una instancia nueva cada vez que alguien pide el servicio.
builder.Services.AddTransient<ITextFormatter, TextFormatter>();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>();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>();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>();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>();Esa separación es una de las bases para hacer pruebas unitarias sin tener que arrancar media aplicación.