Section 6 of 6

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:

Factory Interface C#
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;
    }
}
Factory Implementation C#
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}")
        };
    }
}
Registration C#
builder.Services.AddScoped<EmailNotificationService>();
builder.Services.AddScoped<SmsNotificationService>();
builder.Services.AddScoped<INotificationFactory, NotificationFactory>();
Usage C#
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:

Payment Strategies C#
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);
    }
}
Strategy Selector C#
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);
    }
}
Registration C#
// 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:

Validation Chain C#
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);
    }
}
Chain Executor C#
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;
    }
}
Registration C#
builder.Services.AddScoped<IOrderValidator, StockValidator>();
builder.Services.AddScoped<IOrderValidator, CustomerCreditValidator>();
builder.Services.AddScoped<OrderValidationService>();

4. Decorator Pattern

Add behavior to services without modifying them:

Caching Decorator C#
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;
    }
}
Registration with Scrutor C#
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.Decorate<IProductService, CachedProductService>();

5. Repository + Unit of Work

Unit of Work Pattern C#
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();
}
Usage C#
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

Injecting Options C#
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:

Lazy<T> Injection C#
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();
        }
    }
}
Registration C#
builder.Services.AddTransient<IPdfGenerator, PdfGenerator>();
builder.Services.AddTransient(sp => new Lazy<IPdfGenerator>(
    () => sp.GetRequiredService<IPdfGenerator>()));

Complete InvenTrack Example

Program.cs (All Patterns) C#
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
🎉 Part IV Complete!

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! 🚀