Constructor Injection
🎯 What You'll Learn
- Why constructor injection is preferred
- Best practices for constructor injection
- Handling multiple dependencies
- Optional dependencies
- Avoiding constructor over-injection
- Null checking and validation
- Primary constructors (C# 12)
- Common patterns for InvenTrack
Why Constructor Injection?
Constructor injection is the recommended way to inject dependencies because:
- Explicit dependencies: Clear what a class needs
- Immutability: Dependencies can't change after construction
- Testability: Easy to provide mocks in tests
- Fail-fast: Missing dependencies cause immediate errors
- No nulls: Dependencies are guaranteed to exist
Basic Constructor Injection
public class ProductService : IProductService
{
private readonly IProductRepository _repository;
private readonly ILogger<ProductService> _logger;
public ProductService(
IProductRepository repository,
ILogger<ProductService> logger)
{
_repository = repository;
_logger = logger;
}
public async Task<Product?> GetByIdAsync(int id)
{
_logger.LogInformation("Getting product with ID {Id}", id);
return await _repository.GetByIdAsync(id);
}
}
Always use readonly for injected dependencies. This ensures they can't be
accidentally reassigned after construction.
Multiple Dependencies
It's common to inject multiple dependencies:
public class OrderService : IOrderService
{
private readonly IOrderRepository _orderRepository;
private readonly IProductRepository _productRepository;
private readonly ICustomerRepository _customerRepository;
private readonly IEmailService _emailService;
private readonly ILogger<OrderService> _logger;
public OrderService(
IOrderRepository orderRepository,
IProductRepository productRepository,
ICustomerRepository customerRepository,
IEmailService emailService,
ILogger<OrderService> logger)
{
_orderRepository = orderRepository;
_productRepository = productRepository;
_customerRepository = customerRepository;
_emailService = emailService;
_logger = logger;
}
}
If you have more than 5-6 dependencies, your class might be doing too much. Consider splitting it into smaller, focused services (Single Responsibility Principle).
Optional Dependencies
Sometimes a dependency is optional. You can handle this in two ways:
Method 1: Nullable Parameter (Not Recommended)
public class ProductService
{
private readonly ICacheService? _cache;
public ProductService(ICacheService? cache = null)
{
_cache = cache;
}
public async Task<Product?> GetByIdAsync(int id)
{
if (_cache != null)
{
var cached = await _cache.GetAsync<Product>($"product:{id}");
if (cached != null) return cached;
}
// Get from database...
}
}
Problem: Null checks everywhere, harder to test.
Method 2: Null Object Pattern (Recommended)
// Null implementation that does nothing
public class NullCacheService : ICacheService
{
public Task<T?> GetAsync<T>(string key) => Task.FromResult<T?>(default);
public Task SetAsync<T>(string key, T value) => Task.CompletedTask;
}
// Register the null implementation when caching is disabled
if (builder.Configuration.GetValue<bool>("EnableCaching"))
{
builder.Services.AddSingleton<ICacheService, RedisCacheService>();
}
else
{
builder.Services.AddSingleton<ICacheService, NullCacheService>();
}
// Now ProductService always has a cache, no null checks needed!
public class ProductService
{
private readonly ICacheService _cache; // No nullable!
public ProductService(ICacheService cache)
{
_cache = cache;
}
}
Null Checking and Validation
Traditional Null Checks
public class ProductService
{
private readonly IProductRepository _repository;
public ProductService(IProductRepository repository)
{
_repository = repository ?? throw new ArgumentNullException(nameof(repository));
}
}
C# 11: Required Members (Not for DI)
// This doesn't work with DI container!
public class ProductService
{
public required IProductRepository Repository { get; set; }
}
Modern Approach: Trust the Container
public class ProductService
{
private readonly IProductRepository _repository;
public ProductService(IProductRepository repository)
{
_repository = repository; // No null check needed!
}
}
The DI container guarantees that all dependencies are resolved. If a dependency is missing, the app fails at startup (fail-fast), not at runtime. Null checks are redundant.
Primary Constructors (C# 12)
C# 12 introduced primary constructors for classes, which can simplify DI:
Traditional Constructor
public class ProductService : IProductService
{
private readonly IProductRepository _repository;
private readonly ILogger<ProductService> _logger;
public ProductService(
IProductRepository repository,
ILogger<ProductService> logger)
{
_repository = repository;
_logger = logger;
}
public async Task<Product?> GetByIdAsync(int id)
{
_logger.LogInformation("Getting product {Id}", id);
return await _repository.GetByIdAsync(id);
}
}
Primary Constructor
public class ProductService(
IProductRepository repository,
ILogger<ProductService> logger) : IProductService
{
public async Task<Product?> GetByIdAsync(int id)
{
logger.LogInformation("Getting product {Id}", id);
return await repository.GetByIdAsync(id);
}
}
Benefits: Less boilerplate, no field declarations or assignments needed!
Primary constructor parameters are not readonly and can be captured by lambdas/closures, which may cause unexpected behavior. Use with caution in complex scenarios.
Avoiding Constructor Over-Injection
If your constructor has too many parameters, consider these solutions:
1. Split the Class
public class OrderService
{
// 8 dependencies - too many!
public OrderService(
IOrderRepository orderRepo,
IProductRepository productRepo,
ICustomerRepository customerRepo,
IInventoryService inventoryService,
IEmailService emailService,
IPaymentService paymentService,
IShippingService shippingService,
ILogger<OrderService> logger) { }
}
public class OrderCreationService
{
public OrderCreationService(
IOrderRepository orderRepo,
IInventoryService inventoryService,
ILogger<OrderCreationService> logger) { }
}
public class OrderFulfillmentService
{
public OrderFulfillmentService(
IOrderRepository orderRepo,
IPaymentService paymentService,
IShippingService shippingService,
IEmailService emailService,
ILogger<OrderFulfillmentService> logger) { }
}
2. Use Facade Pattern
public interface INotificationFacade
{
Task SendOrderConfirmationAsync(Order order);
}
public class NotificationFacade : INotificationFacade
{
private readonly IEmailService _emailService;
private readonly ISmsService _smsService;
private readonly IPushNotificationService _pushService;
public NotificationFacade(
IEmailService emailService,
ISmsService smsService,
IPushNotificationService pushService)
{
_emailService = emailService;
_smsService = smsService;
_pushService = pushService;
}
public async Task SendOrderConfirmationAsync(Order order)
{
await _emailService.SendAsync(order.CustomerEmail, "Order confirmed");
await _smsService.SendAsync(order.CustomerPhone, "Order confirmed");
}
}
// Now OrderService only needs INotificationFacade
public class OrderService
{
public OrderService(
IOrderRepository orderRepo,
INotificationFacade notifications) { }
}
Complete InvenTrack Examples
Controller with DI
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
private readonly ILogger<ProductsController> _logger;
public ProductsController(
IProductService productService,
ILogger<ProductsController> logger)
{
_productService = productService;
_logger = logger;
}
[HttpGet("{id}")]
public async Task<ActionResult<Product>> GetById(int id)
{
_logger.LogInformation("Getting product {Id}", id);
var product = await _productService.GetByIdAsync(id);
if (product == null)
return NotFound();
return Ok(product);
}
}
Service with Multiple Dependencies
public class InventoryService : IInventoryService
{
private readonly IProductRepository _productRepository;
private readonly IEmailService _emailService;
private readonly ILogger<InventoryService> _logger;
public InventoryService(
IProductRepository productRepository,
IEmailService emailService,
ILogger<InventoryService> logger)
{
_productRepository = productRepository;
_emailService = emailService;
_logger = logger;
}
public async Task CheckLowStockAsync()
{
var lowStockProducts = await _productRepository
.GetLowStockProductsAsync(threshold: 10);
foreach (var product in lowStockProducts)
{
_logger.LogWarning("Low stock: {Product}", product.Name);
await _emailService.SendLowStockAlertAsync(product);
}
}
}
Key Takeaways
- Constructor injection is the preferred DI method
- Use
readonlyfields for injected dependencies - Dependencies are explicit and immutable
- No null checks needed—trust the container
- For optional dependencies, use the Null Object Pattern
- Primary constructors (C# 12) reduce boilerplate
- More than 5-6 dependencies? Consider splitting the class
- Use Facade pattern to group related services
- Fail-fast: Missing dependencies cause startup errors
- Keep constructors simple—just assign dependencies
You now understand constructor injection best practices! In the final section, we'll explore Common DI Patterns—factory pattern, strategy pattern, chain of responsibility, and other advanced techniques for building InvenTrack.