La inyección de dependencias (DI) en ASP.NET Core no es un accesorio: es un ciudadano de primera clase. El contenedor viene integrado desde el primer día y forma parte del arranque mismo de la aplicación.
Cuando la usamos bien, conseguimos aplicaciones mucho más fáciles de:
- Mantener
- Probar
- Entender (separación de responsabilidades)
- Escalar
Pero cuando la usamos mal… los problemas pueden ser silenciosos y caros: fugas de memoria, race conditions difíciles de reproducir, dependencias circulares, servicios que terminan fuertemente acoplados sin que nos demos cuenta.
En este post te comparto 12 reglas prácticas que he visto que marcan la diferencia en proyectos reales (y que intento seguir yo mismo para no sufrir después).
Primero, entendamos los lifetimes (muy rápido)
ASP.NET Core maneja tres ciclos de vida principales, y el orden importa muchísimo:
Singleton → Scoped → Transient
Es decir: un servicio de mayor duración no debería depender de uno que viva menos tiempo.
Regla de oro: un servicio solo debería depender de servicios que tengan el mismo o menor tiempo de vida que él en esa jerarquía. Si rompes esta regla aparece el clásico problema de captive dependency (dependencia cautiva), y suele doler bastante cuando te das cuenta en producción.
1. Transient para servicios livianos y sin estado
services.AddTransient<IEmailFormatter, EmailFormatter>();
Cuándo elegir Transient:
- Helpers, formateadores, mappers
- Servicios puramente funcionales (sin estado)
- Operaciones rápidas y baratas
Cada vez que alguien lo pide, se crea una instancia nueva.
Cuidado: si el constructor hace trabajo pesado (lectura de archivos grandes, conexiones iniciales costosas, etc.) o si se resuelve cientos de veces por request… piénsalo dos veces.
2. Scoped para todo lo que deba vivir “durante una petición HTTP”
services.AddScoped<IOrderService, OrderService>();
El caso estrella es el DbContext de Entity Framework Core:
services.AddDbContext<AppDbContext>();
Scoped asegura que durante toda la request HTTP tendrás la misma instancia. Esto es clave para mantener coherencia en transacciones, change tracking, etc.
Nunca registres un DbContext como Singleton. Es una de las formas más rápidas de romper una aplicación en producción.
3. Singleton solo cuando realmente es seguro
services.AddSingleton<ICacheService, MemoryCacheService>();
Un Singleton vive toda la vida de la aplicación. Por lo tanto:
- Tiene que ser thread-safe (o sincronizar muy bien lo que toque)
- No debe guardar estado específico de un usuario o de una request
- Jamás debe depender de servicios Scoped
Un buen candidato a Singleton suele ser un servicio completamente stateless o uno que maneja recursos compartidos de forma segura (por ejemplo caching).
Si metes un Singleton que no es thread-safe o que captura dependencias más cortas… prepárate para dolores de cabeza intermitentes que son infernales de debuggear.
4. Siempre inyecta interfaces, nunca implementaciones concretas
Bien:
public class OrderService : IOrderService
{
private readonly IPaymentGateway _gateway;
public OrderService(IPaymentGateway gateway)
{
_gateway = gateway;
}
}
Mal (evítalo):
public OrderService(PaymentGateway gateway) { … }
Inyectar la interfaz reduce el acoplamiento y te permite hacer mocks fácilmente en pruebas unitarias. Es una de las bases del “por qué usamos DI” en primer lugar.
5. Si el constructor parece una lista de supermercado, algo huele mal
Ejemplo sospechoso:
public OrderService(
IRepository repo,
ILogger logger,
IMapper mapper,
ICache cache,
IValidator validator,
IEmailService email)
{ … }
Cuando un constructor recibe 5–7 dependencias (o más), casi siempre estás violando el principio de responsabilidad única (SRP).
Antes de añadir la siguiente dependencia, párate y pregúntate:
“¿Esta clase está haciendo demasiadas cosas? ¿Podría dividirla en dos servicios más pequeños y enfocados?”
6. Nunca, jamás, inyectes un servicio Scoped dentro de un Singleton
Este es probablemente el error #1 que veo en revisiones de código y en producción.
Si un Singleton captura un Scoped, rompes la jerarquía de lifetimes –> captive dependency.
- En el mejor caso –> excepción al arrancar (si tienes validación activada)
- En el peor caso –> datos obsoletos, fugas de memoria o comportamiento impredecible que solo aparece con carga real
Evítalo a toda costa.
7. Cuando de verdad necesites un Scoped desde un Singleton… usa IServiceScopeFactory
Típico en BackgroundService, Hosted Services, procesadores en background, etc.:
public class Worker : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
public Worker(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var scope = _scopeFactory.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
// tu lógica aquí, dentro del scope correcto
}
}
De esta forma creas un scope «artificial» solo para esa ejecución y todo queda en su lifetime correcto.
8. No dejes que Program.cs se convierta en un monstruo de 300 líneas
En lugar de registrar todo ahí, organiza con métodos de extensión:
builder.Services.AddInfrastructureServices();
builder.Services.AddApplicationServices();
builder.Services.AddDomainServices();
Ejemplo:
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services)
{
services.AddScoped<IRepository, Repository>();
services.AddSingleton<ICacheService, MemoryCacheService>();
// ...
return services;
}
}
Tu Program.cs queda limpio, legible y mucho más fácil de mantener cuando el proyecto crece.
9. Activa la validación del contenedor (al menos en desarrollo)
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = true;
options.ValidateOnBuild = true;
});
Esto detecta automáticamente:
- Dependencias circulares
- Captive dependencies
- Servicios registrados mal
Es infinitamente mejor que la aplicación te avise al arrancar en local que descubrirlo a las 3 de la mañana en producción.
10. Olvídate del Service Locator (GetService / GetRequiredService fuera del constructor)
Mal (muy mal):
var service = provider.GetService<IOrderService>();
Esto esconde las dependencias reales, complica muchísimo las pruebas y va en contra de la filosofía de DI.
Siempre que puedas: inyección por constructor. Es más explícito y el compilador te ayuda.
11. Caza las dependencias circulares desde el minuto uno
Si A necesita B y B necesita A… el contenedor se ahoga y no puede construir el grafo.
Suele ser señal de que el diseño necesita revisión. Algunas salidas rápidas:
- Extraer la responsabilidad compartida a un tercer servicio
- Crear una interfaz “puente” o evento
- Usar patrones como Mediator / CQRS cuando el dominio lo justifique
12. Usa IOptions (y sus variantes) para configuraciones fuertemente tipadas
En vez de inyectar IConfiguration directamente y hacer .GetSection("MySettings") por todos lados:
public class MyService
{
private readonly MySettings _settings;
public MyService(IOptions<MySettings> options)
{
_settings = options.Value;
}
}
Variantes útiles según el caso:
IOptions–> configuración estática (la más común)IOptionsSnapshot–> se refresca por cada request (útil si necesitas cambios sin reiniciar)IOptionsMonitor–> singleton que permite suscribirse a cambios en vivo
Tipar la configuración mejora muchísimo la legibilidad y reduce errores tontos de nombres de claves.
Errores que más veo repetirse en producción (resumen rápido)
- Singletons que capturan Scoped (el clásico)
- Transient en objetos pesados que se crean 1000 veces por segundo
- Clases “dios” con 12 dependencias y 400 líneas
- Uso de Service Locator “porque es más rápido de escribir”
- DbContext como Singleton (por favor, no)
Recapitulemos para finalizar
La inyección de dependencias no es solo «una forma cool de evitar new». Es una de las palancas más potentes que tienes para que tu código sea mantenible, testable y escalable a medida que el proyecto crece.
Si respetas los lifetimes, mantienes las clases enfocadas, evitas atajos peligrosos y validas temprano… vas a tener aplicaciones mucho más sanas y fáciles de evolucionar.
La DI bien usada pasa desapercibida.
La DI mal usada… se convierte en un dolor constante.
Mejor dejarla bien hecha desde el principio.
¿Cuál de estas 12 reglas te ha dolido más en algún proyecto? ¿O cuál aplicas religiosamente? Si quieres, déjame un comentario me encantaría leer experiencias reales.
Sigamos codificando.