Inyección por constructor e interfaces
Aplique la inyección por constructor con un diseño basado en interfaces para desacoplar componentes y permitir las pruebas unitarias.
Inyección por constructor e interfaces es una lección gratuita de C# Academy en CoddyKit. Esta es la lección 3 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de C# Academy, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de C# Academy incluye 4 lecciones en total.
Fundamentos de la inyección mediante constructor
La inyección mediante constructor es el patrón de DI más habitual. Una clase declara sus dependencias como parámetros del constructor y el contenedor las proporciona al crear la instancia.
Esto hace que las dependencias sean explícitas, obligatorias y visibles.
Definir la interfaz
Comience con una interfaz que defina el contrato. Esto permite intercambiar distintas implementaciones —reales, simuladas o en memoria— sin cambiar el código que depende de ellas.
public interface IProductRepository
{
Task<Product?> GetByIdAsync(int id);
Task<IEnumerable<Product>> GetAllAsync();
Task AddAsync(Product product);
}Implementar la interfaz
La implementación concreta realiza el trabajo real: llamadas a la base de datos, solicitudes HTTP, etc. El consumidor nunca necesita saber qué implementación recibe.
public class SqlProductRepository : IProductRepository
{
private readonly AppDbContext _db;
public SqlProductRepository(AppDbContext db) => _db = db;
public async Task<Product?> GetByIdAsync(int id)
=> await _db.Products.FindAsync(id);
public async Task<IEnumerable<Product>> GetAllAsync()
=> await _db.Products.ToListAsync();
public async Task AddAsync(Product product)
{
_db.Products.Add(product);
await _db.SaveChangesAsync();
}
}Consumir mediante el constructor
La capa de servicios recibe IProductRepository como parámetro del constructor. Nunca sabe nada sobre SqlProductRepository; solo conoce el contrato de la interfaz.
public class ProductService
{
private readonly IProductRepository _repo;
private readonly ILogger<ProductService> _logger;
public ProductService(
IProductRepository repo,
ILogger<ProductService> logger)
{
_repo = repo;
_logger = logger;
}
public async Task<Product?> GetProductAsync(int id)
{
_logger.LogInformation("Fetching product {Id}", id);
return await _repo.GetByIdAsync(id);
}
}Registrar la cadena
Registre todos los servicios de la cadena. El contenedor resuelve automáticamente todo el grafo de dependencias: ProductService → IProductRepository → AppDbContext.
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseSqlite("Data Source=app.db"));
builder.Services.AddScoped<IProductRepository, SqlProductRepository>();
builder.Services.AddScoped<ProductService>();Probar con un mock
Como las dependencias se inyectan mediante interfaces, las pruebas unitarias pueden recibir un mock o un objeto simulado sin acceder a la base de datos. Esta es una de las mayores ventajas de la DI.
// Using NSubstitute or Moq in a unit test
var mockRepo = Substitute.For<IProductRepository>();
mockRepo.GetByIdAsync(1).Returns(new Product { Id = 1, Name = "Widget" });
var logger = Substitute.For<ILogger<ProductService>>();
var svc = new ProductService(mockRepo, logger);
var result = await svc.GetProductAsync(1);
Assert.Equal("Widget", result?.Name);Varios parámetros del constructor
Una clase puede tener muchos parámetros en su constructor. El contenedor los resuelve todos siempre que cada uno esté registrado. Mantenga los constructores centrados; tener demasiados parámetros suele indicar que la clase asume demasiadas responsabilidades.
public class OrderService
{
public OrderService(
IOrderRepository orders,
IProductRepository products,
IEmailSender email,
ILogger<OrderService> logger)
{
// all injected by DI container
}
}Evitar el antipatrón Service Locator
NO inyecte IServiceProvider ni llame a GetService dentro de los métodos de negocio. Esto oculta las dependencias y dificulta las pruebas. Prefiera la inyección mediante constructor para obtener un código explícito y fácil de probar.
// BAD
public void Process(IServiceProvider sp)
{
var repo = sp.GetService<IProductRepository>(); // hidden dep!
}
// GOOD
public class Processor
{
private readonly IProductRepository _repo;
public Processor(IProductRepository repo) => _repo = repo;
}Sintaxis del constructor primario (C# 12)
C# 12 introduce los constructores primarios para todos los tipos de clase, lo que permite declarar los parámetros directamente en la declaración de la clase para obtener un código de DI más conciso.
// C# 12 primary constructor
public class ProductService(
IProductRepository repo,
ILogger<ProductService> logger)
{
public async Task<Product?> GetAsync(int id)
{
logger.LogInformation("Getting {Id}", id);
return await repo.GetByIdAsync(id);
}
}Intercambiar implementaciones
Una ventaja clave de la DI basada en interfaces es que puede intercambiar una implementación en una sola línea sin cambiar el código que la consume. Resulta útil para cambiar de base de datos, proveedor de correo o indicadores de funcionalidades.
// Switch from SQL to in-memory for integration tests
if (environment.IsEnvironment("Test"))
builder.Services.AddScoped<IProductRepository, InMemoryProductRepository>();
else
builder.Services.AddScoped<IProductRepository, SqlProductRepository>();En la práctica: un controlador con DI
Los controladores de ASP.NET Core usan de forma nativa la inyección mediante constructor. El framework resuelve todos los parámetros y los proporciona.
[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
private readonly ProductService _svc;
public ProductsController(ProductService svc) => _svc = svc;
[HttpGet("{id}")]
public async Task<IActionResult> Get(int id)
{
var product = await _svc.GetProductAsync(id);
return product is null ? NotFound() : Ok(product);
}
}Comprobación rápida
¿Cuál es la principal ventaja de programar contra interfaces al usar DI?
Recapitulación: inyección mediante constructor e interfaces
Ideas clave:
- Declare las dependencias como parámetros del constructor; el contenedor las proporciona
- Defina interfaces para los contratos de servicio; inyecte la interfaz, no el tipo concreto
- Las interfaces permiten usar mocks fácilmente en las pruebas unitarias e intercambiar implementaciones
- Los constructores primarios de C# 12 reducen el código repetitivo en las clases con mucha DI
- Evite el antipatrón Service Locator; mantenga las dependencias explícitas
Preguntas frecuentes
¿La lección «Inyección por constructor e interfaces» es gratis?
Sí — el texto completo de «Inyección por constructor e interfaces» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de C# Academy, actualiza a CoddyKit PRO. El curso de C# Academy incluye 4 lecciones en total.
¿Qué aprenderé en «Inyección por constructor e interfaces»?
Aplique la inyección por constructor con un diseño basado en interfaces para desacoplar componentes y permitir las pruebas unitarias. Practicas C# Academy con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.
¿Necesito experiencia previa para empezar C# Academy?
No se requiere experiencia previa. C# Academy en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 3 de 4.
¿Cuánto tiempo toma la lección «Inyección por constructor e interfaces»?
La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.
¿Puedo escribir y ejecutar código en esta lección de C# Academy?
Sí. Cada lección de C# Academy incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.
Todas las lecciones de este curso
- Fundamentos de los contenedores DI
- Ciclos de vida de servicios: Transient, Scoped, Singleton
- Inyección por constructor e interfaces
- Patrones Factory y Options