12 Reglas Prácticas para Sacarle el Máximo Provecho a la Inyección de Dependencias en ASP.NET Core

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.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.