El arte de soltar el control: una guía humana sobre inyección de dependencias en C# trata de desaprender el instinto de controlar todo y confiar en que las herramientas correctas te serán entregadas en el momento adecuado. Este artículo recorre la filosofía, el desafío y la implementación práctica de DI en aplicaciones .NET modernas.
Todo desarrollador ha escrito código así en algún momento: una clase que crea sus propias dependencias, decide sus propios colaboradores y termina convirtiéndose en un monolito de lógica fuertemente acoplada. Se siente natural — necesitas un logger, creas un logger. Necesitas una conexión a la base de datos, instancias una ahí mismo, en línea, con la palabra clave new y una sensación de control total.
El problema no es la palabra clave new. El problema es el supuesto enterrado dentro de ella: que esta clase sabe mejor que nadie lo que necesita y cómo obtenerlo.
Cuando codificas las dependencias directamente, hacer pruebas se vuelve casi imposible. Cambiar implementaciones se convierte en una operación quirúrgica. Tu código se vuelve frágil exactamente en los lugares donde necesita ser flexible — en las costuras entre componentes.
La Inyección de Dependencias no es un patrón sobre frameworks o contenedores. Es un cambio de mentalidad: deja de construir cosas que saben cómo adquirir lo que necesitan, y empieza a construir cosas que simplemente declaran lo que necesitan. Deja que algo más — un contenedor, una fábrica, un harness de pruebas — lo entregue.
El primer paso es dejar de depender de tipos concretos y comenzar a depender de interfaces. Este es el contrato que tu clase anuncia al mundo.
public interface ITransactionRepository
{
Task<TransactionStatus> GetStatusAsync(Guid transactionId);
Task<bool> CommitAsync(Guid transactionId);
}La lógica de negocio nunca debería importarle si los datos vienen de SQL Server, de un almacenamiento en memoria o de un mock en una prueba unitaria. Solo le importa la forma de los datos.
La inyección por constructor es la forma más explícita y honesta de DI. Hace que las dependencias sean visibles, no opcionales y fáciles de razonar.
public class PaymentProcessor
{
private readonly ITransactionRepository _repository;
private readonly ILogger<PaymentProcessor> _logger;
public PaymentProcessor(
ITransactionRepository repository,
ILogger<PaymentProcessor> logger)
{
_repository = repository;
_logger = logger;
}
public async Task<bool> ProcessAsync(Guid transactionId)
{
_logger.LogInformation("Procesando transacción {Id}", transactionId);
var status = await _repository.GetStatusAsync(transactionId);
if (status != TransactionStatus.Pending)
{
_logger.LogWarning("La transacción {Id} no está en un estado procesable.", transactionId);
return false;
}
return await _repository.CommitAsync(transactionId);
}
}Nota lo que PaymentProcessor NO hace: no crea una conexión a la base de datos, no sabe qué framework de logging se está usando y no decide su propio ciclo de vida. Simplemente hace su trabajo.
En .NET, el contenedor de DI integrado vive en Microsoft.Extensions.DependencyInjection. Registras tus dependencias una vez, al inicio, y el contenedor se encarga de la resolución en todos los demás lugares.
var builder = WebApplication.CreateBuilder(args);
// Registrar el repositorio con tiempo de vida scoped (una vez por petición HTTP)
builder.Services.AddScoped<ITransactionRepository, SqlTransactionRepository>();
// Registrar el procesador — el contenedor inyectará el repositorio automáticamente
builder.Services.AddScoped<PaymentProcessor>();
var app = builder.Build();Entender los tiempos de vida es fundamental:
El verdadero beneficio del DI se revela en el momento de las pruebas. Con dependencias inyectadas, puedes sustituir implementaciones reales por fakes controlados.
public class PaymentProcessorTests
{
[Fact]
public async Task ProcessAsync_RetornaFalso_CuandoTransaccionNoEstaPendiente()
{
// Arrange
var mockRepo = new Mock<ITransactionRepository>();
mockRepo
.Setup(r => r.GetStatusAsync(It.IsAny<Guid>()))
.ReturnsAsync(TransactionStatus.Completed);
var logger = NullLogger<PaymentProcessor>.Instance;
var processor = new PaymentProcessor(mockRepo.Object, logger);
// Act
var result = await processor.ProcessAsync(Guid.NewGuid());
// Assert
Assert.False(result);
}
}Sin base de datos. Sin archivos de configuración. Sin variables de entorno. Solo lógica pura, probada en aislamiento.
Cuando este procesador se expone a través de un endpoint de API, la petición entrante debe transportar la siguiente estructura. El controlador la recibe, extrae el ID de transacción y delega al PaymentProcessor inyectado.
{
"transaction_id": "f3a2d1b0-4e5f-6789-abcd-ef0123456789",
"requested_by": "user-service",
"idempotency_key": "pay-20260615-00042",
"metadata": {
"source": "mobile-checkout",
"retry_attempt": 0,
"currency": "EUR"
}
}La idempotency_key es especialmente importante en contextos financieros: garantiza que aunque la misma petición se envíe dos veces por un reintento de red, la operación solo se ejecute una vez en el backend.
La Inyección de Dependencias no es sobre frameworks. No es sobre anotaciones, contenedores ni el atributo [Inject]. Es sobre la disciplina de construir componentes que declaran sus necesidades en lugar de satisfacerlas unilateralmente.
Cuando sueltas la palabra clave new — cuando tus clases dejan de adquirir sus propias dependencias — algo interesante ocurre. Tu código se vuelve más fácil de leer, porque las dependencias son explícitas. Se vuelve más fácil de probar, porque puedes sustituir cualquier cosa. Se vuelve más fácil de mantener, porque cambiar una implementación no se propaga por todo el código.

SOFTWARE ARCHITECT
Arquitecto de Software especializado en fintech, sistemas europeos de alta disponibilidad y migraciones progresivas de monolitos a microservicios.