Common DI Patterns
🎯 What You'll Learn
- Factory Pattern with DI
- Strategy Pattern for runtime behavior selection
- Chain of Responsibility Pattern
- Decorator Pattern for cross-cutting concerns
- Repository Pattern with Unit of Work
- Options Pattern integration
- Lazy initialization
- Practical InvenTrack examples
1. Factory Pattern
Use factories when you need to create instances at runtime based on conditions:
public interface INotificationService
{
Task SendAsync(string recipient, string message);
}
public class EmailNotificationService : INotificationService
{
public Task SendAsync(string recipient, string message)
{
Console.WriteLine($"Email to {recipient}: {message}");
return Task.CompletedTask;
}
}
public class SmsNotificationService : INotificationService
{
public Task SendAsync(string recipient, string message)
{
Console.WriteLine($"SMS to {recipient}: {message}");
return Task.CompletedTask;
}
}
public enum NotificationType
{
Email,
Sms
}
public interface INotificationFactory
{
INotificationService Create(NotificationType type);
}
public class NotificationFactory : INotificationFactory
{
private readonly IServiceProvider _serviceProvider;
public NotificationFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public INotificationService Create(NotificationType type)
{
return type switch
{
NotificationType.Email => _serviceProvider.GetRequiredService<EmailNotificationService>(),
NotificationType.Sms => _serviceProvider.GetRequiredService<SmsNotificationService>(),
_ => throw new ArgumentException($"Unknown type: {type}")
};
}
}
builder.Services.AddScoped<EmailNotificationService>();
builder.Services.AddScoped<SmsNotificationService>();
builder.Services.AddScoped<INotificationFactory, NotificationFactory>();
public class OrderService
{
private readonly INotificationFactory _notificationFactory;
public OrderService(INotificationFactory notificationFactory)
{
_notificationFactory = notificationFactory;
}
public async Task NotifyCustomerAsync(NotificationType type, string recipient)
{
var service = _notificationFactory.Create(type);
await service.SendAsync(recipient, "Your order is ready!");
}
}
2. Strategy Pattern
Select behavior at runtime based on context:
public interface IPaymentStrategy
{
string Name { get; }
Task<bool> ProcessPaymentAsync(decimal amount);
}
public class CreditCardPaymentStrategy : IPaymentStrategy
{
public string Name => "CreditCard";
public Task<bool> ProcessPaymentAsync(decimal amount)
{
Console.WriteLine($"Processing credit card payment: {amount:C}");
return Task.FromResult(true);
}
}
public class PayPalPaymentStrategy : IPaymentStrategy
{
public string Name => "PayPal";
public Task<bool> ProcessPaymentAsync(decimal amount)
{
Console.WriteLine($"Processing PayPal payment: {amount:C}");
return Task.FromResult(true);
}
}
public class PaymentService
{
private readonly IEnumerable<IPaymentStrategy> _strategies;
public PaymentService(IEnumerable<IPaymentStrategy> strategies)
{
_strategies = strategies;
}
public async Task<bool> ProcessPaymentAsync(string method, decimal amount)
{
var strategy = _strategies.FirstOrDefault(s => s.Name == method);
if (strategy == null)
throw new InvalidOperationException($"Unknown payment method: {method}");
return await strategy.ProcessPaymentAsync(amount);
}
}
// Register all strategies
builder.Services.AddScoped<IPaymentStrategy, CreditCardPaymentStrategy>();
builder.Services.AddScoped<IPaymentStrategy, PayPalPaymentStrategy>();
builder.Services.AddScoped<PaymentService>();
3. Chain of Responsibility
Process a request through a chain of handlers:
public interface IOrderValidator
{
Task<bool> ValidateAsync(Order order);
}
public class StockValidator : IOrderValidator
{
private readonly IProductRepository _productRepository;
public StockValidator(IProductRepository productRepository)
{
_productRepository = productRepository;
}
public async Task<bool> ValidateAsync(Order order)
{
foreach (var item in order.Items)
{
var product = await _productRepository.GetByIdAsync(item.ProductId);
if (product.QuantityInStock < item.Quantity)
return false;
}
return true;
}
}
public class CustomerCreditValidator : IOrderValidator
{
public Task<bool> ValidateAsync(Order order)
{
// Check customer credit limit
return Task.FromResult(true);
}
}
public class OrderValidationService
{
private readonly IEnumerable<IOrderValidator> _validators;
public OrderValidationService(IEnumerable<IOrderValidator> validators)
{
_validators = validators;
}
public async Task<bool> ValidateOrderAsync(Order order)
{
foreach (var validator in _validators)
{
if (!await validator.ValidateAsync(order))
return false;
}
return true;
}
}
builder.Services.AddScoped<IOrderValidator, StockValidator>();
builder.Services.AddScoped<IOrderValidator, CustomerCreditValidator>();
builder.Services.AddScoped<OrderValidationService>();
4. Decorator Pattern
Add behavior to services without modifying them:
public class CachedProductService : IProductService
{
private readonly IProductService _inner;
private readonly ICacheService _cache;
public CachedProductService(IProductService inner, ICacheService cache)
{
_inner = inner;
_cache = cache;
}
public async Task<Product?> GetByIdAsync(int id)
{
var cacheKey = $"product:{id}";
// Try cache first
var cached = await _cache.GetAsync<Product>(cacheKey);
if (cached != null) return cached;
// Call inner service
var product = await _inner.GetByIdAsync(id);
// Cache result
if (product != null)
{
await _cache.SetAsync(cacheKey, product, TimeSpan.FromMinutes(5));
}
return product;
}
}
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.Decorate<IProductService, CachedProductService>();
5. Repository + Unit of Work
public interface IUnitOfWork : IDisposable
{
IProductRepository Products { get; }
ICustomerRepository Customers { get; }
IOrderRepository Orders { get; }
Task<int> SaveChangesAsync();
}
public class UnitOfWork : IUnitOfWork
{
private readonly InvenTrackDbContext _context;
public UnitOfWork(InvenTrackDbContext context)
{
_context = context;
Products = new ProductRepository(context);
Customers = new CustomerRepository(context);
Orders = new OrderRepository(context);
}
public IProductRepository Products { get; }
public ICustomerRepository Customers { get; }
public IOrderRepository Orders { get; }
public Task<int> SaveChangesAsync() => _context.SaveChangesAsync();
public void Dispose() => _context.Dispose();
}
public class OrderService
{
private readonly IUnitOfWork _unitOfWork;
public OrderService(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task CreateOrderAsync(Order order)
{
// All changes in one transaction
await _unitOfWork.Orders.AddAsync(order);
foreach (var item in order.Items)
{
var product = await _unitOfWork.Products.GetByIdAsync(item.ProductId);
product.QuantityInStock -= item.Quantity;
}
await _unitOfWork.SaveChangesAsync();
}
}
6. Options Pattern Integration
public class EmailSettings
{
public string SmtpServer { get; set; } = string.Empty;
public int Port { get; set; }
public string FromAddress { get; set; } = string.Empty;
}
public class EmailService : IEmailService
{
private readonly EmailSettings _settings;
public EmailService(IOptions<EmailSettings> options)
{
_settings = options.Value;
}
public Task SendAsync(string to, string subject, string body)
{
Console.WriteLine($"Sending via {_settings.SmtpServer}:{_settings.Port}");
return Task.CompletedTask;
}
}
7. Lazy Initialization
Delay expensive service creation until needed:
public class ReportService
{
private readonly Lazy<IPdfGenerator> _pdfGenerator;
public ReportService(Lazy<IPdfGenerator> pdfGenerator)
{
_pdfGenerator = pdfGenerator;
}
public async Task GenerateReportAsync(bool includePdf)
{
// PDF generator only created if needed
if (includePdf)
{
var generator = _pdfGenerator.Value; // Created here!
await generator.GenerateAsync();
}
}
}
builder.Services.AddTransient<IPdfGenerator, PdfGenerator>();
builder.Services.AddTransient(sp => new Lazy<IPdfGenerator>(
() => sp.GetRequiredService<IPdfGenerator>()));
Complete InvenTrack Example
var builder = WebApplication.CreateBuilder(args);
// Framework services
builder.Services.AddControllers();
// Database
builder.Services.AddDbContext<InvenTrackDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// Unit of Work
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
// Business services
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.Decorate<IProductService, CachedProductService>(); // Decorator
// Strategy Pattern - Payment methods
builder.Services.AddScoped<IPaymentStrategy, CreditCardPaymentStrategy>();
builder.Services.AddScoped<IPaymentStrategy, PayPalPaymentStrategy>();
builder.Services.AddScoped<PaymentService>();
// Chain of Responsibility - Validators
builder.Services.AddScoped<IOrderValidator, StockValidator>();
builder.Services.AddScoped<IOrderValidator, CustomerCreditValidator>();
builder.Services.AddScoped<OrderValidationService>();
// Factory Pattern
builder.Services.AddScoped<EmailNotificationService>();
builder.Services.AddScoped<SmsNotificationService>();
builder.Services.AddScoped<INotificationFactory, NotificationFactory>();
// Options Pattern
builder.Services.Configure<EmailSettings>(builder.Configuration.GetSection("Email"));
var app = builder.Build();
app.MapControllers();
app.Run();
Key Takeaways
- Factory Pattern: Create instances at runtime based on conditions
- Strategy Pattern: Select behavior at runtime (inject
IEnumerable<T>) - Chain of Responsibility: Process through multiple handlers
- Decorator Pattern: Add behavior without modifying services (use Scrutor)
- Unit of Work: Coordinate multiple repositories in one transaction
- Options Pattern: Inject
IOptions<T>for configuration - Lazy<T>: Delay expensive service creation
- All patterns work seamlessly with DI
- Combine patterns for powerful, flexible architectures
Congratulations! You've completed Part IV: Dependency Injection. You now understand:
- ✅ What Dependency Injection is and why it matters
- ✅ The built-in DI container
- ✅ Service lifetimes (Transient, Scoped, Singleton)
- ✅ Registering services (basic, generic, conditional)
- ✅ Constructor injection best practices
- ✅ Common DI patterns (Factory, Strategy, Chain, Decorator, etc.)
Next up: Part V will explore Building RESTful APIs— controllers, routing, model binding, validation, and creating a complete API for InvenTrack. The journey continues! 🚀