Inyección de Dependencias en C#: qué es y por qué es necesaria
Dependency Injection (DI) es uno de los patrones de diseño clave que permite crear aplicaciones en C# débilmente acopladas y fácilmente testeables. En lugar de que una clase cree sus propias dependencias (por ejemplo, objetos para trabajar con la base de datos o el registro de logs), estas dependencias se pasan (inyectan) desde el exterior. Esto implementa el principio de Inversion of Control (IoC) — el control se transfiere a un contenedor externo.
En el ecosistema .NET, DI se ha convertido en el estándar de facto desde ASP.NET Core. El contenedor de dependencias integrado (a través de Microsoft.Extensions.DependencyInjection) permite registrar y resolver dependencias fácilmente. En este artículo, analizaremos la teoría, ejemplos prácticos y mejores prácticas del uso de DI en C#.
Principios básicos de la Inyección de Dependencias
DI se basa en tres formas fundamentales de inyectar dependencias. Veamos cada una de ellas.
1. Inyección por constructor
Es la forma más común y preferida. Las dependencias se pasan a través de los parámetros del constructor de la clase.
public interface ILogger{ void Log(string message);}
public class ConsoleLogger : ILogger{ public void Log(string message) => Console.WriteLine(message);}
public class OrderService{ private readonly ILogger _logger;
// Inyección de dependencia por constructor public OrderService(ILogger logger) { _logger = logger; }
public void ProcessOrder(int orderId) { _logger.Log($"Procesando pedido {orderId}"); // lógica de negocio }}2. Inyección por propiedad
La dependencia se establece a través de una propiedad pública. Es menos preferible, ya que hace que la dependencia sea opcional y puede provocar errores en tiempo de ejecución.
public class OrderService{ public ILogger Logger { get; set; }
public void ProcessOrder(int orderId) { if (Logger == null) throw new InvalidOperationException("Logger no está configurado"); Logger.Log($"Procesando pedido {orderId}"); }}3. Inyección por método
La dependencia se pasa como un parámetro del método. Se usa raramente, generalmente para dependencias temporales.
public class OrderService{ public void ProcessOrder(int orderId, ILogger logger) { logger.Log($"Procesando pedido {orderId}"); }}Registro de dependencias en el contenedor de .NET
En el .NET moderno (ASP.NET Core, Console, Worker Services) se utiliza el contenedor DI integrado. El registro se realiza en el método ConfigureServices o al crear un ServiceCollection.
Existen tres tiempos de vida (lifetime) para los servicios registrados:
- Transient — se crea una nueva instancia cada vez que se solicita. Adecuado para servicios ligeros y de corta duración.
- Scoped — una instancia por cada solicitud HTTP (o scope). Se usa para Entity Framework DbContext.
- Singleton — una instancia para toda la aplicación. Adecuado para cachés, configuraciones, loggers.
// Ejemplo para ASP.NET Corepublic class Startup{ public void ConfigureServices(IServiceCollection services) { // Registro con interfaz services.AddTransient<ILogger, ConsoleLogger>(); services.AddScoped<IOrderRepository, OrderRepository>(); services.AddSingleton<IConfiguration, ConfigurationManager>();
// Registro sin interfaz (clase concreta) services.AddTransient<OrderService>(); }}
// Uso en un controladorpublic class OrderController : ControllerBase{ private readonly OrderService _orderService;
public OrderController(OrderServi