What is Dependency Injection?
π― What You'll Learn
- What Dependency Injection (DI) is and why it matters
- The problem DI solves
- Understanding dependencies and coupling
- The Dependency Inversion Principle (SOLID)
- Benefits of using DI
- DI vs Service Locator pattern
- How DI fits into InvenTrack
The Problem: Tight Coupling
Let's start with a problem. Imagine you're building InvenTrack and need to send email notifications when inventory is low:
public class InventoryService
{
public void CheckStock(Product product)
{
if (product.QuantityInStock < 10)
{
// Creating dependency directly - TIGHT COUPLING!
var emailService = new EmailService();
emailService.SendLowStockAlert(product.Name);
}
}
}
Problems with This Approach
- Hard to Test: Can't test without sending real emails
- Hard to Change: Want to use a different email provider? Modify every class
- Hard to Reuse:
InventoryServiceis tied toEmailService - Violates SOLID: Depends on concrete implementation, not abstraction
The Solution: Dependency Injection
Dependency Injection is a design pattern where objects receive their dependencies from external sources rather than creating them.
// 1. Define an interface (abstraction)
public interface IEmailService
{
void SendLowStockAlert(string productName);
}
// 2. Implement the interface
public class EmailService : IEmailService
{
public void SendLowStockAlert(string productName)
{
// Send actual email
Console.WriteLine($"Sending low stock alert for {productName}");
}
}
// 3. Inject the dependency via constructor
public class InventoryService
{
private readonly IEmailService _emailService;
// Dependency is INJECTED, not created
public InventoryService(IEmailService emailService)
{
_emailService = emailService;
}
public void CheckStock(Product product)
{
if (product.QuantityInStock < 10)
{
_emailService.SendLowStockAlert(product.Name);
}
}
}
InventoryService now depends on the abstraction
(IEmailService), not the concrete implementation (EmailService).
This is the Dependency Inversion Principle from SOLID.
Benefits of This Approach
- Easy to Test: Inject a mock
IEmailServicein tests - Easy to Change: Swap implementations without changing
InventoryService - Reusable:
InventoryServiceworks with anyIEmailService - Follows SOLID: Depends on abstractions, not concretions
What is a Dependency?
A dependency is any object that another object needs to function.
public class ProductService
{
// These are ALL dependencies:
private readonly IProductRepository _repository; // Database access
private readonly ILogger<ProductService> _logger; // Logging
private readonly IEmailService _emailService; // Email
private readonly ICacheService _cacheService; // Caching
public ProductService(
IProductRepository repository,
ILogger<ProductService> logger,
IEmailService emailService,
ICacheService cacheService)
{
_repository = repository;
_logger = logger;
_emailService = emailService;
_cacheService = cacheService;
}
}
The Dependency Inversion Principle
The D in SOLID stands for Dependency Inversion Principle:
High-level modules should not depend on low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.
Before DIP (Bad)
InventoryService β EmailService (concrete class)
β
Tightly coupled to implementation
After DIP (Good)
InventoryService β IEmailService (interface)
β
EmailService (implements interface)
β
Both depend on abstraction
Types of Dependency Injection
1. Constructor Injection (Recommended)
public class ProductService
{
private readonly IProductRepository _repository;
public ProductService(IProductRepository repository)
{
_repository = repository;
}
}
β Preferred: Dependencies are clear, immutable, and required
2. Property Injection (Rare)
public class ProductService
{
public IProductRepository Repository { get; set; }
}
β οΈ Avoid: Dependencies are optional and mutable
3. Method Injection (Specific Cases)
public class ProductService
{
public void ProcessProduct(Product product, ILogger logger)
{
logger.LogInformation($"Processing {product.Name}");
}
}
β οΈ Rare: Only when dependency varies per method call
Use Constructor Injection 99% of the time. It makes dependencies explicit and ensures objects are always in a valid state.
Benefits of Dependency Injection
1. Testability
[Fact]
public void CheckStock_WhenLow_SendsEmail()
{
// Arrange - use a MOCK email service
var mockEmailService = new Mock<IEmailService>();
var service = new InventoryService(mockEmailService.Object);
var product = new Product { Name = "Laptop", QuantityInStock = 5 };
// Act
service.CheckStock(product);
// Assert - verify email was sent
mockEmailService.Verify(x => x.SendLowStockAlert("Laptop"), Times.Once);
}
2. Flexibility
Easily swap implementations:
// Development: Log emails to console
public class ConsoleEmailService : IEmailService
{
public void SendLowStockAlert(string productName)
{
Console.WriteLine($"[DEV] Low stock: {productName}");
}
}
// Production: Send real emails via SendGrid
public class SendGridEmailService : IEmailService
{
public void SendLowStockAlert(string productName)
{
// Send via SendGrid API
}
}
3. Maintainability
Changes to EmailService don't affect InventoryService as long
as the interface stays the same.
4. Separation of Concerns
Each class has a single responsibility. InventoryService manages inventory,
EmailService sends emails.
DI Container (IoC Container)
A DI Container (also called an IoC Container - Inversion of Control) is a framework that manages object creation and dependency resolution.
// You have to manually create everything
var emailService = new EmailService();
var inventoryService = new InventoryService(emailService);
var controller = new ProductsController(inventoryService);
// Register services once in Program.cs
builder.Services.AddScoped<IEmailService, EmailService>();
builder.Services.AddScoped<IInventoryService, InventoryService>();
// Container automatically creates and injects dependencies!
// ProductsController gets InventoryService, which gets EmailService
When ASP.NET Core needs a ProductsController, the DI container:
1. Sees it needs IInventoryService
2. Creates InventoryService
3. Sees InventoryService needs IEmailService
4. Creates EmailService
5. Injects EmailService into InventoryService
6. Injects InventoryService into ProductsController
Key Takeaways
- Dependency Injection is a design pattern for loose coupling
- Objects receive dependencies instead of creating them
- Depend on abstractions (interfaces), not concrete classes
- Dependency Inversion Principle: Both high and low-level modules depend on abstractions
- Constructor Injection is the preferred method
- Benefits: Testability, flexibility, maintainability, separation of concerns
- A DI Container manages object creation and dependency resolution
- ASP.NET Core has a built-in DI container
- Register services in
Program.cs, inject via constructor
You now understand what Dependency Injection is and why it's essential! In the next section, we'll explore The Built-in DI Container in ASP.NET Coreβhow to register services, resolve dependencies, and use the container effectively.