![design-pattern]()
Previous article: ASP.NET Core Configuration & Secrets Mastery: Complete Security Guide (Part- 17 of 40)
Table of Contents
The Design Patterns Renaissance
Repository Pattern: Data Access Abstraction
Unit of Work: Transaction Management
Singleton Pattern: Shared Instance Management
Factory Pattern: Object Creation Magic
Strategy Pattern: Algorithmic Flexibility
Decorator Pattern: Behavior Extension
Observer Pattern: Event-Driven Architecture
Mediator Pattern: Decoupled Communication
Pattern Combinations in Real Applications
Testing Patterns with xUnit
Performance Considerations
Anti-Patterns to Avoid
Conclusion
1. The Design Patterns Renaissance
Why Design Patterns Matter in Modern Development
Design patterns are proven solutions to common software design problems. In ASP.NET Core, they provide:
Maintainability : Code that's easy to understand and modify
Scalability : Architecture that grows with your application
Testability : Designs that support comprehensive testing
Flexibility : Systems that adapt to changing requirements
Real-World Analogy: The Construction Blueprint
Think of design patterns like architectural blueprints in construction:
// Without patterns - like building without blueprintspublic class MessyController : ControllerBase{
private readonly MyDbContext _context;
private readonly IConfiguration _config;
private readonly ILogger _logger;
public IActionResult GetUser(int id)
{
// Direct database access
// Business logic mixed with data access
// Hard to test and maintain
var user = _context.Users.Find(id);
if (user == null) return NotFound();
// Validation logic here
// Email sending logic here
// Logging scattered everywhere
return Ok(user);
}}
// With patterns - like following proven architectural planspublic class CleanController : ControllerBase{
private readonly IUserRepository _userRepository;
private readonly IUserService _userService;
public CleanController(IUserRepository userRepository, IUserService userService)
{
_userRepository = userRepository;
_userService = userService;
}
public async Task<IActionResult> GetUser(int id)
{
var user = await _userRepository.GetByIdAsync(id);
if (user == null) return NotFound();
return Ok(user);
}}
The Pattern Categories in ASP.NET Core
| Category | Purpose | ASP.NET Core Examples |
|---|
| Creational | Object creation mechanisms | Singleton, Factory, Builder |
| Structural | Object composition | Decorator, Adapter, Composite |
| Behavioral | Object interaction & responsibility | Strategy, Observer, Mediator |
| Architectural | Application structure | Repository, Unit of Work, CQRS |
2. Repository Pattern: Data Access Abstraction
The Problem: Tight Coupling with Data Access
Without the Repository Pattern, controllers become tightly coupled to Entity Framework:
// β Problem: Controller coupled to Entity Frameworkpublic class ProductController : ControllerBase{
private readonly ApplicationDbContext _context;
public ProductController(ApplicationDbContext context)
{
_context = context;
}
public async Task<IActionResult> GetProducts()
{
// Direct EF Core usage throughout controller
var products = await _context.Products
.Include(p => p.Category)
.Where(p => p.IsActive)
.ToListAsync();
return Ok(products);
}
// Multiple actions with similar data access logic
// Hard to test without database
// Difficult to change data storage technology}
Repository Pattern Solution
The Repository pattern mediates between the domain and data mapping layers, acting like an in-memory domain object collection.
Step 1. Define Repository Interfaces
// Interfaces/IRepository.csusing System.Linq.Expressions;
namespace ECommercePatterns.Interfaces{
/// <summary>
/// Base repository interface with common CRUD operations
/// </summary>
/// <typeparam name="T">Entity type</typeparam>
public interface IRepository<T> where T : class
{
Task<T?> GetByIdAsync(int id);
Task<IEnumerable<T>> GetAllAsync();
Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
Task<T> AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(T entity);
Task<bool> ExistsAsync(int id);
Task<int> CountAsync(Expression<Func<T, bool>>? predicate = null);
}
/// <summary>
/// Extended repository with pagination and advanced queries
/// </summary>
/// <typeparam name="T">Entity type</typeparam>
public interface IExtendedRepository<T> : IRepository<T> where T : class
{
Task<PagedResult<T>> GetPagedAsync(int page, int pageSize);
Task<PagedResult<T>> GetPagedAsync(Expression<Func<T, bool>> predicate, int page, int pageSize);
Task<IEnumerable<T>> GetAsync(ISpecification<T> specification);
Task<T?> GetSingleAsync(Expression<Func<T, bool>> predicate);
}
/// <summary>
/// Product-specific repository operations
/// </summary>
public interface IProductRepository : IExtendedRepository<Product>
{
Task<IEnumerable<Product>> GetProductsByCategoryAsync(int categoryId);
Task<IEnumerable<Product>> GetActiveProductsAsync();
Task<IEnumerable<Product>> SearchProductsAsync(string searchTerm);
Task UpdateStockAsync(int productId, int quantity);
Task<IEnumerable<Product>> GetProductsWithCategoryAsync();
Task<PagedResult<Product>> GetProductsPagedWithCategoryAsync(int page, int pageSize);
}
/// <summary>
/// Specification pattern for complex queries
/// </summary>
/// <typeparam name="T">Entity type</typeparam>
public interface ISpecification<T>
{
Expression<Func<T, bool>> Criteria { get; }
List<Expression<Func<T, object>>> Includes { get; }
Expression<Func<T, object>> OrderBy { get; }
Expression<Func<T, object>> OrderByDescending { get; }
int Take { get; }
int Skip { get; }
bool IsPagingEnabled { get; }
}
/// <summary>
/// Pagination result wrapper
/// </summary>
/// <typeparam name="T">Item type</typeparam>
public class PagedResult<T>
{
public IEnumerable<T> Items { get; set; } = new List<T>();
public int TotalCount { get; set; }
public int PageNumber { get; set; }
public int PageSize { get; set; }
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
public bool HasPrevious => PageNumber > 1;
public bool HasNext => PageNumber < TotalPages;
}}
Step 2. Implement Generic Repository
// Repositories/Repository.csusing Microsoft.EntityFrameworkCore;using System.Linq.Expressions;
namespace ECommercePatterns.Repositories{
/// <summary>
/// Generic repository implementation using Entity Framework Core
/// </summary>
/// <typeparam name="T">Entity type</typeparam>
public class Repository<T> : IRepository<T> where T : class
{
protected readonly DbContext _context;
protected readonly DbSet<T> _dbSet;
public Repository(DbContext context)
{
_context = context;
_dbSet = context.Set<T>();
}
public virtual async Task<T?> GetByIdAsync(int id)
{
return await _dbSet.FindAsync(id);
}
public virtual async Task<IEnumerable<T>> GetAllAsync()
{
return await _dbSet.ToListAsync();
}
public virtual async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.Where(predicate).ToListAsync();
}
public virtual async Task<T> AddAsync(T entity)
{
await _dbSet.AddAsync(entity);
await _context.SaveChangesAsync();
return entity;
}
public virtual async Task UpdateAsync(T entity)
{
_dbSet.Update(entity);
await _context.SaveChangesAsync();
}
public virtual async Task DeleteAsync(T entity)
{
_dbSet.Remove(entity);
await _context.SaveChangesAsync();
}
public virtual async Task<bool> ExistsAsync(int id)
{
var entity = await GetByIdAsync(id);
return entity != null;
}
public virtual async Task<int> CountAsync(Expression<Func<T, bool>>? predicate = null)
{
return predicate == null
? await _dbSet.CountAsync()
: await _dbSet.CountAsync(predicate);
}
}
/// <summary>
/// Extended repository with advanced features
/// </summary>
/// <typeparam name="T">Entity type</typeparam>
public class ExtendedRepository<T> : Repository<T>, IExtendedRepository<T> where T : class
{
public ExtendedRepository(DbContext context) : base(context)
{
}
public virtual async Task<PagedResult<T>> GetPagedAsync(int page, int pageSize)
{
return await GetPagedAsync(x => true, page, pageSize);
}
public virtual async Task<PagedResult<T>> GetPagedAsync(Expression<Func<T, bool>> predicate, int page, int pageSize)
{
var query = _dbSet.Where(predicate);
var totalCount = await query.CountAsync();
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new PagedResult<T>
{
Items = items,
TotalCount = totalCount,
PageNumber = page,
PageSize = pageSize
};
}
public virtual async Task<IEnumerable<T>> GetAsync(ISpecification<T> specification)
{
return await ApplySpecification(specification).ToListAsync();
}
public virtual async Task<T?> GetSingleAsync(Expression<Func<T, bool>> predicate)
{
return await _dbSet.FirstOrDefaultAsync(predicate);
}
private IQueryable<T> ApplySpecification(ISpecification<T> specification)
{
return SpecificationEvaluator<T>.GetQuery(_dbSet.AsQueryable(), specification);
}
}
/// <summary>
/// Specification evaluator for applying specifications to queries
/// </summary>
/// <typeparam name="T">Entity type</typeparam>
public static class SpecificationEvaluator<T> where T : class
{
public static IQueryable<T> GetQuery(IQueryable<T> inputQuery, ISpecification<T> specification)
{
var query = inputQuery;
// Apply criteria (where clause)
if (specification.Criteria != null)
{
query = query.Where(specification.Criteria);
}
// Apply includes (eager loading)
query = specification.Includes.Aggregate(query,
(current, include) => current.Include(include));
// Apply ordering
if (specification.OrderBy != null)
{
query = query.OrderBy(specification.OrderBy);
}
else if (specification.OrderByDescending != null)
{
query = query.OrderByDescending(specification.OrderByDescending);
}
// Apply paging
if (specification.IsPagingEnabled)
{
query = query.Skip(specification.Skip)
.Take(specification.Take);
}
return query;
}
}}
Step 3: Implement Specific Repository
// Repositories/ProductRepository.csusing Microsoft.EntityFrameworkCore;using ECommercePatterns.Interfaces;using ECommercePatterns.Entities;
namespace ECommercePatterns.Repositories{
/// <summary>
/// Product-specific repository implementation
/// </summary>
public class ProductRepository : ExtendedRepository<Product>, IProductRepository
{
private readonly ECommerceContext _context;
public ProductRepository(ECommerceContext context) : base(context)
{
_context = context;
}
public async Task<IEnumerable<Product>> GetProductsByCategoryAsync(int categoryId)
{
return await _context.Products
.Include(p => p.Category)
.Where(p => p.CategoryId == categoryId && p.IsActive)
.ToListAsync();
}
public async Task<IEnumerable<Product>> GetActiveProductsAsync()
{
return await _context.Products
.Include(p => p.Category)
.Where(p => p.IsActive)
.OrderBy(p => p.Name)
.ToListAsync();
}
public async Task<IEnumerable<Product>> SearchProductsAsync(string searchTerm)
{
return await _context.Products
.Include(p => p.Category)
.Where(p => p.IsActive &&
(p.Name.Contains(searchTerm) ||
p.Description.Contains(searchTerm) ||
p.Category.Name.Contains(searchTerm)))
.ToListAsync();
}
public async Task UpdateStockAsync(int productId, int quantity)
{
var product = await _context.Products.FindAsync(productId);
if (product != null)
{
product.StockQuantity += quantity;
product.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
}
}
public async Task<IEnumerable<Product>> GetProductsWithCategoryAsync()
{
return await _context.Products
.Include(p => p.Category)
.Where(p => p.IsActive)
.ToListAsync();
}
public async Task<PagedResult<Product>> GetProductsPagedWithCategoryAsync(int page, int pageSize)
{
var query = _context.Products
.Include(p => p.Category)
.Where(p => p.IsActive)
.OrderBy(p => p.Name);
var totalCount = await query.CountAsync();
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return new PagedResult<Product>
{
Items = items,
TotalCount = totalCount,
PageNumber = page,
PageSize = pageSize
};
}
}}
Step 4. Entity Models
// Entities/Product.csusing System.ComponentModel.DataAnnotations;using System.ComponentModel.DataAnnotations.Schema;
namespace ECommercePatterns.Entities{
/// <summary>
/// Product entity representing items in the e-commerce system
/// </summary>
public class Product
{
[Key]
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; } = string.Empty;
[StringLength(500)]
public string Description { get; set; } = string.Empty;
[Column(TypeName = "decimal(18,2)")]
public decimal Price { get; set; }
public int StockQuantity { get; set; }
public bool IsActive { get; set; } = true;
public int CategoryId { get; set; }
[ForeignKey("CategoryId")]
public virtual Category Category { get; set; } = null!;
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
// Business logic methods
public bool IsInStock() => StockQuantity > 0;
public bool CanOrderQuantity(int quantity) => IsActive && StockQuantity >= quantity;
public void ReduceStock(int quantity)
{
if (quantity <= 0)
throw new ArgumentException("Quantity must be positive", nameof(quantity));
if (StockQuantity < quantity)
throw new InvalidOperationException("Insufficient stock");
StockQuantity -= quantity;
UpdatedAt = DateTime.UtcNow;
}
}
/// <summary>
/// Product category entity
/// </summary>
public class Category
{
[Key]
public int Id { get; set; }
[Required]
[StringLength(50)]
public string Name { get; set; } = string.Empty;
[StringLength(200)]
public string Description { get; set; } = string.Empty;
public bool IsActive { get; set; } = true;
public virtual ICollection<Product> Products { get; set; } = new List<Product>();
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}}
Step 5. Database Context
// Data/ECommerceContext.csusing Microsoft.EntityFrameworkCore;using ECommercePatterns.Entities;
namespace ECommercePatterns.Data{
/// <summary>
/// Entity Framework database context for e-commerce application
/// </summary>
public class ECommerceContext : DbContext
{
public ECommerceContext(DbContextOptions<ECommerceContext> options) : base(options)
{
}
public DbSet<Product> Products { get; set; } = null!;
public DbSet<Category> Categories { get; set; } = null!;
public DbSet<Order> Orders { get; set; } = null!;
public DbSet<OrderItem> OrderItems { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Product configuration
modelBuilder.Entity<Product>(entity =>
{
entity.HasKey(p => p.Id);
entity.Property(p => p.Name).IsRequired().HasMaxLength(100);
entity.Property(p => p.Description).HasMaxLength(500);
entity.Property(p => p.Price).HasColumnType("decimal(18,2)");
entity.Property(p => p.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
entity.Property(p => p.UpdatedAt).HasDefaultValueSql("GETUTCDATE()");
// Indexes
entity.HasIndex(p => p.Name);
entity.HasIndex(p => p.CategoryId);
entity.HasIndex(p => p.IsActive);
// Relationships
entity.HasOne(p => p.Category)
.WithMany(c => c.Products)
.HasForeignKey(p => p.CategoryId)
.OnDelete(DeleteBehavior.Restrict);
});
// Category configuration
modelBuilder.Entity<Category>(entity =>
{
entity.HasKey(c => c.Id);
entity.Property(c => c.Name).IsRequired().HasMaxLength(50);
entity.Property(c => c.Description).HasMaxLength(200);
entity.Property(c => c.CreatedAt).HasDefaultValueSql("GETUTCDATE()");
// Indexes
entity.HasIndex(c => c.Name).IsUnique();
});
// Seed data
modelBuilder.Entity<Category>().HasData(
new Category { Id = 1, Name = "Electronics", Description = "Electronic devices and accessories" },
new Category { Id = 2, Name = "Books", Description = "Books and educational materials" },
new Category { Id = 3, Name = "Clothing", Description = "Apparel and fashion items" }
);
modelBuilder.Entity<Product>().HasData(
new Product
{
Id = 1,
Name = "Wireless Mouse",
Description = "Ergonomic wireless mouse",
Price = 29.99m,
StockQuantity = 50,
CategoryId = 1
},
new Product
{
Id = 2,
Name = "Programming Book",
Description = "Advanced C# programming guide",
Price = 45.99m,
StockQuantity = 25,
CategoryId = 2
}
);
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
// Automatic update of UpdatedAt timestamp
var entries = ChangeTracker.Entries()
.Where(e => e.Entity is Product &&
(e.State == EntityState.Modified || e.State == EntityState.Added));
foreach (var entityEntry in entries)
{
if (entityEntry.State == EntityState.Modified)
{
((Product)entityEntry.Entity).UpdatedAt = DateTime.UtcNow;
}
}
return await base.SaveChangesAsync(cancellationToken);
}
}}
Step 6: Clean Controller Using Repository
// Controllers/ProductsController.csusing Microsoft.AspNetCore.Mvc;using ECommercePatterns.Interfaces;using ECommercePatterns.Entities;
namespace ECommercePatterns.Controllers{
/// <summary>
/// Products controller using Repository pattern for clean data access
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class ProductsController : ControllerBase
{
private readonly IProductRepository _productRepository;
private readonly ILogger<ProductsController> _logger;
public ProductsController(IProductRepository productRepository, ILogger<ProductsController> logger)
{
_productRepository = productRepository;
_logger = logger;
}
/// <summary>
/// Get all active products with pagination
/// </summary>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResult<Product>>> GetProducts(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10)
{
_logger.LogInformation("Retrieving products page {Page} with size {PageSize}", page, pageSize);
var result = await _productRepository.GetProductsPagedWithCategoryAsync(page, pageSize);
return Ok(result);
}
/// <summary>
/// Get product by ID
/// </summary>
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<Product>> GetProduct(int id)
{
var product = await _productRepository.GetByIdAsync(id);
if (product == null)
{
_logger.LogWarning("Product with ID {ProductId} not found", id);
return NotFound();
}
return Ok(product);
}
/// <summary>
/// Search products by name or description
/// </summary>
[HttpGet("search")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<Product>>> SearchProducts([FromQuery] string term)
{
if (string.IsNullOrWhiteSpace(term))
{
return BadRequest("Search term is required");
}
var products = await _productRepository.SearchProductsAsync(term);
return Ok(products);
}
/// <summary>
/// Create a new product
/// </summary>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<Product>> CreateProduct(Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
try
{
var createdProduct = await _productRepository.AddAsync(product);
_logger.LogInformation("Created product with ID {ProductId}", createdProduct.Id);
return CreatedAtAction(nameof(GetProduct), new { id = createdProduct.Id }, createdProduct);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating product");
return StatusCode(500, "An error occurred while creating the product");
}
}
/// <summary>
/// Update product stock
/// </summary>
[HttpPatch("{id}/stock")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> UpdateStock(int id, [FromBody] StockUpdateRequest request)
{
try
{
await _productRepository.UpdateStockAsync(id, request.Quantity);
_logger.LogInformation("Updated stock for product {ProductId} by {Quantity}", id, request.Quantity);
return Ok();
}
catch (ArgumentException ex)
{
_logger.LogWarning(ex, "Invalid stock update request for product {ProductId}", id);
return BadRequest(ex.Message);
}
}
}
/// <summary>
/// Stock update request DTO
/// </summary>
public class StockUpdateRequest
{
public int Quantity { get; set; }
}}
Repository Pattern Benefits
β
Separation of Concerns : Data access logic separated from business logic
β
Testability : Easy to mock repositories for unit testing
β
Flexibility : Easy to change data access technology
β
Maintainability : Centralized data access logic
β
Reusability : Generic repository can be used across entities
3. Unit of Work Pattern: Transaction Management
The Problem: Inconsistent Data Operations
Without a Unit of Work, managing transactions across multiple repositories becomes challenging:
// β Problem: Manual transaction management across repositoriespublic class OrderService{
private readonly IProductRepository _productRepository;
private readonly IOrderRepository _orderRepository;
private readonly ApplicationDbContext _context;
public async Task<bool> PlaceOrder(Order order)
{
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Update product stock
foreach (var item in order.Items)
{
var product = await _productRepository.GetByIdAsync(item.ProductId);
product.StockQuantity -= item.Quantity;
await _productRepository.UpdateAsync(product);
}
// Create order
await _orderRepository.AddAsync(order);
await transaction.CommitAsync();
return true;
}
catch
{
await transaction.RollbackAsync();
throw;
}
}}
Unit of Work Solution
The Unit of Work pattern maintains a list of objects affected by a business transaction and coordinates writing out changes and resolving concurrency problems.
Step 1: Unit of Work Interface
// Interfaces/IUnitOfWork.csnamespace ECommercePatterns.Interfaces{
/// <summary>
/// Unit of Work interface for transaction management
/// </summary>
public interface IUnitOfWork : IDisposable
{
IProductRepository Products { get; }
ICategoryRepository Categories { get; }
IOrderRepository Orders { get; }
IUserRepository Users { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
Task BeginTransactionAsync();
Task CommitTransactionAsync();
Task RollbackTransactionAsync();
Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default);
}
/// <summary>
/// Category repository interface
/// </summary>
public interface ICategoryRepository : IExtendedRepository<Category>
{
Task<IEnumerable<Category>> GetActiveCategoriesAsync();
Task<Category?> GetCategoryWithProductsAsync(int categoryId);
}
/// <summary>
/// Order repository interface
/// </summary>
public interface IOrderRepository : IExtendedRepository<Order>
{
Task<IEnumerable<Order>> GetOrdersByUserAsync(int userId);
Task<Order?> GetOrderWithItemsAsync(int orderId);
Task UpdateOrderStatusAsync(int orderId, OrderStatus status);
}
/// <summary>
/// User repository interface
/// </summary>
public interface IUserRepository : IExtendedRepository<User>
{
Task<User?> GetUserByEmailAsync(string email);
Task<bool> UserExistsAsync(string email);
}}
Step 2: Unit of Work Implementation.
// Repositories/UnitOfWork.csusing Microsoft.EntityFrameworkCore.Storage;
namespace ECommercePatterns.Repositories{
/// <summary>
/// Unit of Work implementation coordinating multiple repositories
/// </summary>
public class UnitOfWork : IUnitOfWork
{
private readonly ECommerceContext _context;
private IDbContextTransaction? _transaction;
public UnitOfWork(ECommerceContext context)
{
_context = context;
Products = new ProductRepository(context);
Categories = new CategoryRepository(context);
Orders = new OrderRepository(context);
Users = new UserRepository(context);
}
public IProductRepository Products { get; }
public ICategoryRepository Categories { get; }
public IOrderRepository Orders { get; }
public IUserRepository Users { get; }
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
return await _context.SaveChangesAsync(cancellationToken);
}
public async Task BeginTransactionAsync()
{
_transaction = await _context.Database.BeginTransactionAsync();
}
public async Task CommitTransactionAsync()
{
if (_transaction == null)
{
throw new InvalidOperationException("Transaction not started");
}
try
{
await SaveChangesAsync();
await _transaction.CommitAsync();
}
finally
{
await _transaction.DisposeAsync();
_transaction = null;
}
}
public async Task RollbackTransactionAsync()
{
if (_transaction == null)
{
throw new InvalidOperationException("Transaction not started");
}
await _transaction.RollbackAsync();
await _transaction.DisposeAsync();
_transaction = null;
}
public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)
{
try
{
await SaveChangesAsync(cancellationToken);
return true;
}
catch (Exception ex)
{
// Log exception
return false;
}
}
public void Dispose()
{
_transaction?.Dispose();
_context.Dispose();
}
}}
Step 3: Additional Repository Implementations.
// Repositories/CategoryRepository.csusing Microsoft.EntityFrameworkCore;
namespace ECommercePatterns.Repositories{
public class CategoryRepository : ExtendedRepository<Category>, ICategoryRepository
{
public CategoryRepository(ECommerceContext context) : base(context)
{
}
public async Task<IEnumerable<Category>> GetActiveCategoriesAsync()
{
return await _context.Categories
.Where(c => c.IsActive)
.OrderBy(c => c.Name)
.ToListAsync();
}
public async Task<Category?> GetCategoryWithProductsAsync(int categoryId)
{
return await _context.Categories
.Include(c => c.Products.Where(p => p.IsActive))
.FirstOrDefaultAsync(c => c.Id == categoryId && c.IsActive);
}
}}
// Repositories/OrderRepository.csusing Microsoft.EntityFrameworkCore;
namespace ECommercePatterns.Repositories{
public class OrderRepository : ExtendedRepository<Order>, IOrderRepository
{
public OrderRepository(ECommerceContext context) : base(context)
{
}
public async Task<IEnumerable<Order>> GetOrdersByUserAsync(int userId)
{
return await _context.Orders
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.Where(o => o.UserId == userId)
.OrderByDescending(o => o.CreatedAt)
.ToListAsync();
}
public async Task<Order?> GetOrderWithItemsAsync(int orderId)
{
return await _context.Orders
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.FirstOrDefaultAsync(o => o.Id == orderId);
}
public async Task UpdateOrderStatusAsync(int orderId, OrderStatus status)
{
var order = await _context.Orders.FindAsync(orderId);
if (order != null)
{
order.Status = status;
order.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
}
}
}}
Step 4: Service Using Unit of Work.
// Services/OrderService.csnamespace ECommercePatterns.Services{
/// <summary>
/// Order service using Unit of Work pattern for transaction management
/// </summary>
public interface IOrderService
{
Task<OrderResult> PlaceOrderAsync(CreateOrderRequest request);
Task<bool> CancelOrderAsync(int orderId);
Task<Order?> GetOrderDetailsAsync(int orderId);
}
public class OrderService : IOrderService
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<OrderService> _logger;
public OrderService(IUnitOfWork unitOfWork, ILogger<OrderService> logger)
{
_unitOfWork = unitOfWork;
_logger = logger;
}
public async Task<OrderResult> PlaceOrderAsync(CreateOrderRequest request)
{
await _unitOfWork.BeginTransactionAsync();
try
{
_logger.LogInformation("Placing order for user {UserId}", request.UserId);
// Validate products and stock
var validationResult = await ValidateOrderItemsAsync(request.Items);
if (!validationResult.IsValid)
{
return OrderResult.Failure(validationResult.Errors);
}
// Create order
var order = new Order
{
UserId = request.UserId,
Status = OrderStatus.Pending,
TotalAmount = await CalculateOrderTotalAsync(request.Items),
ShippingAddress = request.ShippingAddress,
Items = request.Items.Select(item => new OrderItem
{
ProductId = item.ProductId,
Quantity = item.Quantity,
UnitPrice = await GetProductPriceAsync(item.ProductId)
}).ToList()
};
// Update product stock
foreach (var item in request.Items)
{
await _unitOfWork.Products.UpdateStockAsync(item.ProductId, -item.Quantity);
}
// Save order
await _unitOfWork.Orders.AddAsync(order);
await _unitOfWork.CommitTransactionAsync();
_logger.LogInformation("Order {OrderId} placed successfully", order.Id);
return OrderResult.Success(order);
}
catch (Exception ex)
{
await _unitOfWork.RollbackTransactionAsync();
_logger.LogError(ex, "Error placing order for user {UserId}", request.UserId);
return OrderResult.Failure(new[] { "An error occurred while placing the order" });
}
}
public async Task<bool> CancelOrderAsync(int orderId)
{
await _unitOfWork.BeginTransactionAsync();
try
{
var order = await _unitOfWork.Orders.GetOrderWithItemsAsync(orderId);
if (order == null || order.Status != OrderStatus.Pending)
{
return false;
}
// Restore product stock
foreach (var item in order.Items)
{
await _unitOfWork.Products.UpdateStockAsync(item.ProductId, item.Quantity);
}
// Update order status
await _unitOfWork.Orders.UpdateOrderStatusAsync(orderId, OrderStatus.Cancelled);
await _unitOfWork.CommitTransactionAsync();
_logger.LogInformation("Order {OrderId} cancelled successfully", orderId);
return true;
}
catch (Exception ex)
{
await _unitOfWork.RollbackTransactionAsync();
_logger.LogError(ex, "Error cancelling order {OrderId}", orderId);
return false;
}
}
public async Task<Order?> GetOrderDetailsAsync(int orderId)
{
return await _unitOfWork.Orders.GetOrderWithItemsAsync(orderId);
}
private async Task<ValidationResult> ValidateOrderItemsAsync(List<OrderItemRequest> items)
{
var errors = new List<string>();
foreach (var item in items)
{
var product = await _unitOfWork.Products.GetByIdAsync(item.ProductId);
if (product == null)
{
errors.Add($"Product with ID {item.ProductId} not found");
}
else if (!product.IsActive)
{
errors.Add($"Product {product.Name} is not available");
}
else if (product.StockQuantity < item.Quantity)
{
errors.Add($"Insufficient stock for {product.Name}. Available: {product.StockQuantity}");
}
}
return new ValidationResult(errors);
}
private async Task<decimal> CalculateOrderTotalAsync(List<OrderItemRequest> items)
{
decimal total = 0;
foreach (var item in items)
{
var price = await GetProductPriceAsync(item.ProductId);
total += price * item.Quantity;
}
return total;
}
private async Task<decimal> GetProductPriceAsync(int productId)
{
var product = await _unitOfWork.Products.GetByIdAsync(productId);
return product?.Price ?? 0;
}
}
/// <summary>
/// Order operation result
/// </summary>
public class OrderResult
{
public bool Success { get; }
public Order? Order { get; }
public IEnumerable<string> Errors { get; }
private OrderResult(bool success, Order? order, IEnumerable<string> errors)
{
Success = success;
Order = order;
Errors = errors;
}
public static OrderResult Success(Order order) => new OrderResult(true, order, Array.Empty<string>());
public static OrderResult Failure(IEnumerable<string> errors) => new OrderResult(false, null, errors);
}
/// <summary>
/// Validation result for order items
/// </summary>
public class ValidationResult
{
public bool IsValid => !Errors.Any();
public List<string> Errors { get; }
public ValidationResult(List<string> errors)
{
Errors = errors;
}
}}
4. Singleton Pattern: Shared Instance Management
The Problem: Uncontrolled Instance Creation
Without proper singleton management, you might create multiple instances unnecessarily:
// β Problem: Multiple instances causing resource wastepublic class CacheService{
private readonly Dictionary<string, object> _cache = new();
public void Set(string key, object value) => _cache[key] = value;
public object Get(string key) => _cache.GetValueOrDefault(key);}
// Multiple instances createdvar cache1 = new CacheService();var cache2 = new CacheService();// cache1 and cache2 have different data - inconsistent!
Singleton Pattern Solutions
Approach 1: ASP.NET Core Built-in Singleton
// Services/ICacheService.csnamespace ECommercePatterns.Services{
/// <summary>
/// Cache service interface for application-level caching
/// </summary>
public interface ICacheService
{
Task<T?> GetAsync<T>(string key);
Task SetAsync<T>(string key, T value, TimeSpan? expiration = null);
Task RemoveAsync(string key);
Task<bool> ExistsAsync(string key);
Task ClearAsync();
}
/// <summary>
/// In-memory cache service implementation as singleton
/// </summary>
public class MemoryCacheService : ICacheService
{
private readonly ConcurrentDictionary<string, CacheEntry> _cache = new();
private readonly ILogger<MemoryCacheService> _logger;
public MemoryCacheService(ILogger<MemoryCacheService> logger)
{
_logger = logger;
_logger.LogInformation("MemoryCacheService initialized");
}
public Task<T?> GetAsync<T>(string key)
{
if (_cache.TryGetValue(key, out var entry) && !IsExpired(entry))
{
_logger.LogDebug("Cache hit for key: {Key}", key);
return Task.FromResult((T?)entry.Value);
}
_logger.LogDebug("Cache miss for key: {Key}", key);
return Task.FromResult(default(T));
}
public Task SetAsync<T>(string key, T value, TimeSpan? expiration = null)
{
var entry = new CacheEntry
{
Value = value,
Expiration = expiration.HasValue ? DateTime.UtcNow.Add(expiration.Value) : null
};
_cache.AddOrUpdate(key, entry, (k, old) => entry);
_logger.LogDebug("Cache set for key: {Key} with expiration: {Expiration}", key, expiration);
return Task.CompletedTask;
}
public Task RemoveAsync(string key)
{
_cache.TryRemove(key, out _);
_logger.LogDebug("Cache removed for key: {Key}", key);
return Task.CompletedTask;
}
public Task<bool> ExistsAsync(string key)
{
var exists = _cache.TryGetValue(key, out var entry) && !IsExpired(entry);
return Task.FromResult(exists);
}
public Task ClearAsync()
{
_cache.Clear();
_logger.LogInformation("Cache cleared");
return Task.CompletedTask;
}
private bool IsExpired(CacheEntry entry)
{
return entry.Expiration.HasValue && entry.Expiration.Value < DateTime.UtcNow;
}
// Background cleanup of expired entries
public void CleanupExpiredEntries()
{
var expiredKeys = _cache.Where(kvp => IsExpired(kvp.Value))
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in expiredKeys)
{
_cache.TryRemove(key, out _);
}
if (expiredKeys.Count > 0)
{
_logger.LogInformation("Cleaned up {Count} expired cache entries", expiredKeys.Count);
}
}
private class CacheEntry
{
public object? Value { get; set; }
public DateTime? Expiration { get; set; }
}
}}
Approach 2: Configuration Singleton
// Services/AppConfigService.csnamespace ECommercePatterns.Services{
/// <summary>
/// Application configuration service with singleton behavior
/// </summary>
public interface IAppConfigService
{
string GetSetting(string key);
T GetSetting<T>(string key, T defaultValue = default!);
void UpdateSetting(string key, string value);
IReadOnlyDictionary<string, string> GetAllSettings();
}
public class AppConfigService : IAppConfigService
{
private readonly ConcurrentDictionary<string, string> _settings;
private readonly IConfiguration _configuration;
private readonly ILogger<AppConfigService> _logger;
// Static instance for true singleton (use carefully!)
private static AppConfigService? _instance;
private static readonly object _lockObject = new object();
public AppConfigService(IConfiguration configuration, ILogger<AppConfigService> logger)
{
_configuration = configuration;
_logger = logger;
_settings = LoadSettingsFromConfig();
// Singleton instance management
lock (_lockObject)
{
if (_instance == null)
{
_instance = this;
}
}
_logger.LogInformation("AppConfigService initialized with {Count} settings", _settings.Count);
}
public static AppConfigService? Instance => _instance;
public string GetSetting(string key)
{
if (_settings.TryGetValue(key, out var value))
{
return value;
}
// Fallback to configuration
var configValue = _configuration[key];
if (!string.IsNullOrEmpty(configValue))
{
_settings[key] = configValue;
return configValue;
}
throw new KeyNotFoundException($"Setting '{key}' not found");
}
public T GetSetting<T>(string key, T defaultValue = default!)
{
try
{
var value = GetSetting(key);
return (T)Convert.ChangeType(value, typeof(T));
}
catch
{
return defaultValue;
}
}
public void UpdateSetting(string key, string value)
{
_settings.AddOrUpdate(key, value, (k, old) => value);
_logger.LogInformation("Setting updated: {Key} = {Value}", key, value);
}
public IReadOnlyDictionary<string, string> GetAllSettings()
{
return new ReadOnlyDictionary<string, string>(_settings);
}
private ConcurrentDictionary<string, string> LoadSettingsFromConfig()
{
var settings = new ConcurrentDictionary<string, string>();
// Load from appsettings.json
foreach (var setting in _configuration.AsEnumerable())
{
if (!string.IsNullOrEmpty(setting.Value))
{
settings[setting.Key] = setting.Value;
}
}
return settings;
}
}}
Approach 3: Lazy Singleton with Dependency Injection.
// Services/GlobalStatisticsService.csnamespace ECommercePatterns.Services{
/// <summary>
/// Global application statistics service using lazy singleton pattern
/// </summary>
public interface IGlobalStatisticsService
{
Task<AppStatistics> GetStatisticsAsync();
Task IncrementRequestCountAsync();
Task RecordErrorAsync(string errorType);
Task RecordOrderAsync(decimal amount);
}
public class GlobalStatisticsService : IGlobalStatisticsService
{
private readonly AppStatistics _statistics = new();
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
private readonly ILogger<GlobalStatisticsService> _logger;
// Lazy singleton instance
private static readonly Lazy<GlobalStatisticsService> _instance =
new Lazy<GlobalStatisticsService>(() => new GlobalStatisticsService());
public static GlobalStatisticsService Instance => _instance.Value;
private GlobalStatisticsService()
{
_logger = LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger<GlobalStatisticsService>();
_logger.LogInformation("GlobalStatisticsService singleton initialized");
}
public async Task<AppStatistics> GetStatisticsAsync()
{
await _lock.WaitAsync();
try
{
return _statistics.Clone();
}
finally
{
_lock.Release();
}
}
public async Task IncrementRequestCountAsync()
{
await _lock.WaitAsync();
try
{
_statistics.TotalRequests++;
_statistics.LastUpdated = DateTime.UtcNow;
_logger.LogDebug("Request count incremented to {Count}", _statistics.TotalRequests);
}
finally
{
_lock.Release();
}
}
public async Task RecordErrorAsync(string errorType)
{
await _lock.WaitAsync();
try
{
_statistics.TotalErrors++;
if (_statistics.ErrorCounts.ContainsKey(errorType))
{
_statistics.ErrorCounts[errorType]++;
}
else
{
_statistics.ErrorCounts[errorType] = 1;
}
_statistics.LastUpdated = DateTime.UtcNow;
_logger.LogWarning("Error recorded: {ErrorType}. Total errors: {TotalErrors}",
errorType, _statistics.TotalErrors);
}
finally
{
_lock.Release();
}
}
public async Task RecordOrderAsync(decimal amount)
{
await _lock.WaitAsync();
try
{
_statistics.TotalOrders++;
_statistics.TotalRevenue += amount;
_statistics.LastUpdated = DateTime.UtcNow;
_logger.LogInformation("Order recorded: Amount {Amount}. Total revenue: {TotalRevenue}",
amount, _statistics.TotalRevenue);
}
finally
{
_lock.Release();
}
}
}
/// <summary>
/// Application statistics data model
/// </summary>
public class AppStatistics
{
public long TotalRequests { get; set; }
public long TotalOrders { get; set; }
public long TotalErrors { get; set; }
public decimal TotalRevenue { get; set; }
public DateTime LastUpdated { get; set; }
public Dictionary<string, int> ErrorCounts { get; set; } = new();
public AppStatistics Clone()
{
return new AppStatistics
{
TotalRequests = TotalRequests,
TotalOrders = TotalOrders,
TotalErrors = TotalErrors,
TotalRevenue = TotalRevenue,
LastUpdated = LastUpdated,
ErrorCounts = new Dictionary<string, int>(ErrorCounts)
};
}
}}
Dependency Injection Registration
// Program.cs - Singleton Registration
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
builder.Services.AddSingleton<IAppConfigService, AppConfigService>();
builder.Services.AddSingleton<IGlobalStatisticsService, GlobalStatisticsService>();
// Background service for cache cleanup
builder.Services.AddHostedService<CacheCleanupService>();
// Services/CacheCleanupService.csnamespace ECommercePatterns.Services{
/// <summary>
/// Background service for cache maintenance
/// </summary>
public class CacheCleanupService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<CacheCleanupService> _logger;
private readonly TimeSpan _cleanupInterval = TimeSpan.FromMinutes(30);
public CacheCleanupService(IServiceProvider serviceProvider, ILogger<CacheCleanupService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Cache cleanup service started");
while (!stoppingToken.IsCancellationRequested)
{
try
{
using var scope = _serviceProvider.CreateScope();
var cacheService = scope.ServiceProvider.GetRequiredService<ICacheService>() as MemoryCacheService;
cacheService?.CleanupExpiredEntries();
_logger.LogDebug("Cache cleanup completed");
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during cache cleanup");
}
await Task.Delay(_cleanupInterval, stoppingToken);
}
}
}}
5. Factory Pattern: Object Creation Magic
The Problem: Complex Object Creation Logic
Without the Factory pattern, object creation logic spreads throughout the application:
// β Problem: Complex object creation scattered everywherepublic class PaymentProcessor{
public IPaymentMethod CreatePaymentMethod(string type, PaymentInfo info)
{
switch (type.ToLower())
{
case "creditcard":
return new CreditCardPayment
{
CardNumber = info.CardNumber,
ExpiryDate = info.ExpiryDate,
CVV = info.CVV
};
case "paypal":
return new PayPalPayment
{
Email = info.Email,
TransactionId = info.TransactionId
};
case "crypto":
return new CryptoPayment
{
WalletAddress = info.WalletAddress,
Currency = info.Currency
};
default:
throw new ArgumentException($"Unknown payment type: {type}");
}
}}
// Same switch statements repeated in multiple places// Hard to maintain and extend
Factory Pattern Solutions
Approach 1: Simple Factory Pattern
// Patterns/Factories/IPaymentMethod.csnamespace ECommercePatterns.Patterns.Factories{
/// <summary>
/// Payment method interface
/// </summary>
public interface IPaymentMethod
{
string Type { get; }
Task<PaymentResult> ProcessPaymentAsync(decimal amount);
bool Validate();
}
/// <summary>
/// Payment information DTO
/// </summary>
public class PaymentInfo
{
public string? CardNumber { get; set; }
public string? ExpiryDate { get; set; }
public string? CVV { get; set; }
public string? Email { get; set; }
public string? TransactionId { get; set; }
public string? WalletAddress { get; set; }
public string? Currency { get; set; }
}
/// <summary>
/// Payment result
/// </summary>
public class PaymentResult
{
public bool Success { get; set; }
public string TransactionId { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
public DateTime ProcessedAt { get; set; } = DateTime.UtcNow;
}
/// <summary>
/// Simple payment factory
/// </summary>
public interface IPaymentFactory
{
IPaymentMethod CreatePaymentMethod(string type, PaymentInfo paymentInfo);
IEnumerable<string> GetAvailablePaymentMethods();
}
public class PaymentFactory : IPaymentFactory
{
private readonly ILogger<PaymentFactory> _logger;
public PaymentFactory(ILogger<PaymentFactory> logger)
{
_logger = logger;
}
public IPaymentMethod CreatePaymentMethod(string type, PaymentInfo paymentInfo)
{
_logger.LogInformation("Creating payment method of type: {Type}", type);
return type.ToLower() switch
{
"creditcard" => new CreditCardPayment(paymentInfo),
"paypal" => new PayPalPayment(paymentInfo),
"crypto" => new CryptoPayment(paymentInfo),
"applepay" => new ApplePayPayment(paymentInfo),
_ => throw new ArgumentException($"Unsupported payment type: {type}")
};
}
public IEnumerable<string> GetAvailablePaymentMethods()
{
return new[] { "creditcard", "paypal", "crypto", "applepay" };
}
}}
Approach 2: Abstract Factory Pattern
// Patterns/Factories/AbstractFactory.csnamespace ECommercePatterns.Patterns.Factories{
/// <summary>
/// Abstract factory for creating payment processors
/// </summary>
public interface IPaymentProcessorFactory
{
IPaymentProcessor CreateProcessor(string countryCode);
IRefundProcessor CreateRefundProcessor(string countryCode);
}
/// <summary>
/// Payment processor interface
/// </summary>
public interface IPaymentProcessor
{
Task<PaymentResult> ProcessAsync(decimal amount, IPaymentMethod paymentMethod);
Task<bool> ValidateAsync(IPaymentMethod paymentMethod);
}
/// <summary>
/// Refund processor interface
/// </summary>
public interface IRefundProcessor
{
Task<RefundResult> ProcessRefundAsync(string transactionId, decimal amount);
}
/// <summary>
/// Concrete factory for US payment systems
/// </summary>
public class USPaymentProcessorFactory : IPaymentProcessorFactory
{
private readonly ILogger<USPaymentProcessorFactory> _logger;
public USPaymentProcessorFactory(ILogger<USPaymentProcessorFactory> logger)
{
_logger = logger;
}
public IPaymentProcessor CreateProcessor(string countryCode)
{
_logger.LogInformation("Creating US payment processor for country: {Country}", countryCode);
return new USPaymentProcessor();
}
public IRefundProcessor CreateRefundProcessor(string countryCode)
{
_logger.LogInformation("Creating US refund processor for country: {Country}", countryCode);
return new USRefundProcessor();
}
}
/// <summary>
/// Concrete factory for EU payment systems
/// </summary>
public class EUPaymentProcessorFactory : IPaymentProcessorFactory
{
private readonly ILogger<EUPaymentProcessorFactory> _logger;
public EUPaymentProcessorFactory(ILogger<EUPaymentProcessorFactory> logger)
{
_logger = logger;
}
public IPaymentProcessor CreateProcessor(string countryCode)
{
_logger.LogInformation("Creating EU payment processor for country: {Country}", countryCode);
return new EUPaymentProcessor();
}
public IRefundProcessor CreateRefundProcessor(string countryCode)
{
_logger.LogInformation("Creating EU refund processor for country: {Country}", countryCode);
return new EURefundProcessor();
}
}}
Approach 3: Factory Method Pattern
// Patterns/Factories/FactoryMethod.csnamespace ECommercePatterns.Patterns.Factories{
/// <summary>
/// Base creator class with factory method
/// </summary>
public abstract class ReportGenerator
{
protected readonly ILogger<ReportGenerator> _logger;
protected ReportGenerator(ILogger<ReportGenerator> logger)
{
_logger = logger;
}
// Factory method
public abstract IReport CreateReport();
// Business logic that uses the factory method
public async Task<ReportResult> GenerateReportAsync()
{
_logger.LogInformation("Starting report generation");
var report = CreateReport();
var result = await report.GenerateAsync();
_logger.LogInformation("Report generation completed: {Success}", result.Success);
return result;
}
}
/// <summary>
/// Concrete creator for sales reports
/// </summary>
public class SalesReportGenerator : ReportGenerator
{
public SalesReportGenerator(ILogger<SalesReportGenerator> logger) : base(logger)
{
}
public override IReport CreateReport()
{
_logger.LogDebug("Creating sales report");
return new SalesReport();
}
}
/// <summary>
/// Concrete creator for inventory reports
/// </summary>
public class InventoryReportGenerator : ReportGenerator
{
public InventoryReportGenerator(ILogger<InventoryReportGenerator> logger) : base(logger)
{
}
public override IReport CreateReport()
{
_logger.LogDebug("Creating inventory report");
return new InventoryReport();
}
}
/// <summary>
/// Report interface
/// </summary>
public interface IReport
{
string ReportType { get; }
Task<ReportResult> GenerateAsync();
}
/// <summary>
/// Sales report implementation
/// </summary>
public class SalesReport : IReport
{
public string ReportType => "Sales";
public async Task<ReportResult> GenerateAsync()
{
// Simulate report generation
await Task.Delay(1000);
return new ReportResult
{
Success = true,
ReportData = "Sales report data...",
GeneratedAt = DateTime.UtcNow
};
}
}
/// <summary>
/// Inventory report implementation
/// </summary>
public class InventoryReport : IReport
{
public string ReportType => "Inventory";
public async Task<ReportResult> GenerateAsync()
{
// Simulate report generation
await Task.Delay(1500);
return new ReportResult
{
Success = true,
ReportData = "Inventory report data...",
GeneratedAt = DateTime.UtcNow
};
}
}
/// <summary>
/// Report generation result
/// </summary>
public class ReportResult
{
public bool Success { get; set; }
public string ReportData { get; set; } = string.Empty;
public DateTime GeneratedAt { get; set; }
public string? ErrorMessage { get; set; }
}}
Concrete Payment Implementations
// Patterns/Factories/PaymentImplementations.csnamespace ECommercePatterns.Patterns.Factories{
/// <summary>
/// Credit card payment implementation
/// </summary>
public class CreditCardPayment : IPaymentMethod
{
private readonly PaymentInfo _paymentInfo;
public string Type => "CreditCard";
public CreditCardPayment(PaymentInfo paymentInfo)
{
_paymentInfo = paymentInfo;
}
public async Task<PaymentResult> ProcessPaymentAsync(decimal amount)
{
// Simulate payment processing
await Task.Delay(500);
if (string.IsNullOrEmpty(_paymentInfo.CardNumber) ||
string.IsNullOrEmpty(_paymentInfo.ExpiryDate))
{
return new PaymentResult
{
Success = false,
Message = "Invalid credit card information"
};
}
// Simulate successful payment
return new PaymentResult
{
Success = true,
TransactionId = Guid.NewGuid().ToString(),
Message = "Credit card payment processed successfully"
};
}
public bool Validate()
{
return !string.IsNullOrEmpty(_paymentInfo.CardNumber) &&
!string.IsNullOrEmpty(_paymentInfo.ExpiryDate) &&
!string.IsNullOrEmpty(_paymentInfo.CVV) &&
_paymentInfo.CardNumber.Length == 16 &&
_paymentInfo.CVV.Length == 3;
}
}
/// <summary>
/// PayPal payment implementation
/// </summary>
public class PayPalPayment : IPaymentMethod
{
private readonly PaymentInfo _paymentInfo;
public string Type => "PayPal";
public PayPalPayment(PaymentInfo paymentInfo)
{
_paymentInfo = paymentInfo;
}
public async Task<PaymentResult> ProcessPaymentAsync(decimal amount)
{
// Simulate PayPal payment processing
await Task.Delay(800);
if (string.IsNullOrEmpty(_paymentInfo.Email))
{
return new PaymentResult
{
Success = false,
Message = "Invalid PayPal email"
};
}
// Simulate successful payment
return new PaymentResult
{
Success = true,
TransactionId = Guid.NewGuid().ToString(),
Message = "PayPal payment processed successfully"
};
}
public bool Validate()
{
return !string.IsNullOrEmpty(_paymentInfo.Email) &&
_paymentInfo.Email.Contains('@');
}
}
/// <summary>
/// US payment processor implementation
/// </summary>
public class USPaymentProcessor : IPaymentProcessor
{
public async Task<PaymentResult> ProcessAsync(decimal amount, IPaymentMethod paymentMethod)
{
// US-specific payment processing logic
await Task.Delay(300);
if (amount > 10000)
{
return new PaymentResult
{
Success = false,
Message = "Amount exceeds US regulatory limit"
};
}
return await paymentMethod.ProcessPaymentAsync(amount);
}
public async Task<bool> ValidateAsync(IPaymentMethod paymentMethod)
{
await Task.Delay(100);
return paymentMethod.Validate();
}
}}
Factory Pattern Usage in Controllers
// Controllers/PaymentsController.csusing ECommercePatterns.Patterns.Factories;
namespace ECommercePatterns.Controllers{
/// <summary>
/// Payments controller demonstrating factory pattern usage
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class PaymentsController : ControllerBase
{
private readonly IPaymentFactory _paymentFactory;
private readonly IPaymentProcessorFactory _processorFactory;
private readonly ILogger<PaymentsController> _logger;
public PaymentsController(
IPaymentFactory paymentFactory,
IPaymentProcessorFactory processorFactory,
ILogger<PaymentsController> logger)
{
_paymentFactory = paymentFactory;
_processorFactory = processorFactory;
_logger = logger;
}
[HttpPost]
public async Task<ActionResult<PaymentResult>> ProcessPayment([FromBody] ProcessPaymentRequest request)
{
try
{
_logger.LogInformation("Processing payment of {Amount} via {Method}",
request.Amount, request.PaymentMethod);
// Create payment method using factory
var paymentMethod = _paymentFactory.CreatePaymentMethod(
request.PaymentMethod, request.PaymentInfo);
// Validate payment method
if (!paymentMethod.Validate())
{
return BadRequest("Invalid payment information");
}
// Create appropriate processor based on country
var processor = _processorFactory.CreateProcessor(request.CountryCode);
// Process payment
var result = await processor.ProcessAsync(request.Amount, paymentMethod);
if (result.Success)
{
_logger.LogInformation("Payment processed successfully. Transaction: {TransactionId}",
result.TransactionId);
return Ok(result);
}
else
{
_logger.LogWarning("Payment processing failed: {Message}", result.Message);
return BadRequest(result);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing payment");
return StatusCode(500, "An error occurred while processing payment");
}
}
[HttpGet("methods")]
public ActionResult<IEnumerable<string>> GetAvailablePaymentMethods()
{
var methods = _paymentFactory.GetAvailablePaymentMethods();
return Ok(methods);
}
}
/// <summary>
/// Payment processing request
/// </summary>
public class ProcessPaymentRequest
{
public decimal Amount { get; set; }
public string PaymentMethod { get; set; } = string.Empty;
public string CountryCode { get; set; } = "US";
public PaymentInfo PaymentInfo { get; set; } = new();
}}
6. Strategy Pattern: Algorithmic Flexibility
The Problem: Complex Conditional Logic
Without the Strategy pattern, you end up with complex conditional statements:
// β Problem: Complex conditional logic for different algorithmspublic class ShippingCalculator{
public decimal CalculateShipping(string method, decimal weight, decimal distance)
{
if (method == "standard")
{
return weight * 0.5m + distance * 0.1m;
}
else if (method == "express")
{
return weight * 1.2m + distance * 0.3m + 10m;
}
else if (method == "overnight")
{
return weight * 2.0m + distance * 0.5m + 25m;
}
else if (method == "international")
{
return (weight * 3.0m + distance * 0.8m) * 1.1m; // 10% international fee
}
else
{
throw new ArgumentException($"Unknown shipping method: {method}");
}
}}
Strategy Pattern Solution
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.
Step 1: Define Strategy Interface
// Patterns/Strategies/IShippingStrategy.csnamespace ECommercePatterns.Patterns.Strategies{
/// <summary>
/// Shipping strategy interface
/// </summary>
public interface IShippingStrategy
{
string Name { get; }
decimal CalculateCost(decimal weight, decimal distance, string destination);
TimeSpan EstimateDelivery(decimal distance);
bool IsAvailable(string destination);
}
/// <summary>
/// Shipping calculation context
/// </summary>
public class ShippingContext
{
public decimal Weight { get; set; }
public decimal Distance { get; set; }
public string Destination { get; set; } = string.Empty;
public DateTime OrderDate { get; set; } = DateTime.UtcNow;
}
/// <summary>
/// Shipping calculation result
/// </summary>
public class ShippingResult
{
public decimal Cost { get; set; }
public TimeSpan EstimatedDelivery { get; set; }
public string Method { get; set; } = string.Empty;
public bool IsAvailable { get; set; }
public string? Message { get; set; }
}}
Step 2: Implement Concrete Strategies
// Patterns/Strategies/ShippingStrategies.csnamespace ECommercePatterns.Patterns.Strategies{
/// <summary>
/// Standard shipping strategy
/// </summary>
public class StandardShippingStrategy : IShippingStrategy
{
public string Name => "Standard";
public decimal CalculateCost(decimal weight, decimal distance, string destination)
{
var baseCost = weight * 0.5m + distance * 0.1m;
// Additional charges for remote destinations
if (IsRemoteDestination(destination))
{
baseCost *= 1.2m;
}
return Math.Round(baseCost, 2);
}
public TimeSpan EstimateDelivery(decimal distance)
{
var days = (int)Math.Ceiling(distance / 100); // 100 km per day
return TimeSpan.FromDays(Math.Max(days, 3)); // Minimum 3 days
}
public bool IsAvailable(string destination)
{
return !IsRestrictedDestination(destination);
}
private bool IsRemoteDestination(string destination)
{
var remoteDestinations = new[] { "hawaii", "alaska", "puerto rico" };
return remoteDestinations.Contains(destination.ToLower());
}
private bool IsRestrictedDestination(string destination)
{
var restrictedDestinations = new[] { "cuba", "north korea", "syria" };
return restrictedDestinations.Contains(destination.ToLower());
}
}
/// <summary>
/// Express shipping strategy
/// </summary>
public class ExpressShippingStrategy : IShippingStrategy
{
public string Name => "Express";
public decimal CalculateCost(decimal weight, decimal distance, string destination)
{
var baseCost = weight * 1.2m + distance * 0.3m + 10m; // Base + premium
// Weekend surcharge
if (IsWeekend(DateTime.UtcNow))
{
baseCost += 5m;
}
return Math.Round(baseCost, 2);
}
public TimeSpan EstimateDelivery(decimal distance)
{
var days = (int)Math.Ceiling(distance / 300); // 300 km per day
return TimeSpan.FromDays(Math.Max(days, 2)); // Minimum 2 days
}
public bool IsAvailable(string destination)
{
return !IsRestrictedDestination(destination) &&
!IsRemoteDestination(destination);
}
private bool IsWeekend(DateTime date)
{
return date.DayOfWeek == DayOfWeek.Saturday ||
date.DayOfWeek == DayOfWeek.Sunday;
}
private bool IsRemoteDestination(string destination)
{
var remoteDestinations = new[] { "hawaii", "alaska" };
return remoteDestinations.Contains(destination.ToLower());
}
private bool IsRestrictedDestination(string destination)
{
var restrictedDestinations = new[] { "cuba", "north korea" };
return restrictedDestinations.Contains(destination.ToLower());
}
}
/// <summary>
/// Overnight shipping strategy
/// </summary>
public class OvernightShippingStrategy : IShippingStrategy
{
public string Name => "Overnight";
public decimal CalculateCost(decimal weight, decimal distance, string destination)
{
var baseCost = weight * 2.0m + distance * 0.5m + 25m;
// Urgency premium
if (IsAfterHours(DateTime.UtcNow))
{
baseCost *= 1.5m;
}
return Math.Round(baseCost, 2);
}
public TimeSpan EstimateDelivery(decimal distance)
{
var hours = (int)Math.Ceiling(distance / 50); // 50 km per hour (by air)
return TimeSpan.FromHours(Math.Max(hours, 24)); // Minimum 24 hours
}
public bool IsAvailable(string destination)
{
return !IsRestrictedDestination(destination) &&
distance <= 5000 && // Maximum distance
IsServiceAvailableToday();
}
private bool IsAfterHours(DateTime date)
{
return date.Hour < 8 || date.Hour >= 18; // 6 PM to 8 AM
}
private bool IsRestrictedDestination(string destination)
{
var restrictedDestinations = new[] { "cuba", "north korea", "syria", "iran" };
return restrictedDestinations.Contains(destination.ToLower());
}
private bool IsServiceAvailableToday()
{
var today = DateTime.UtcNow;
return today.DayOfWeek != DayOfWeek.Sunday;
}
}
/// <summary>
/// International shipping strategy
/// </summary>
public class InternationalShippingStrategy : IShippingStrategy
{
public string Name => "International";
public decimal CalculateCost(decimal weight, decimal distance, string destination)
{
var baseCost = (weight * 3.0m + distance * 0.8m) * 1.1m; // 10% international fee
// Customs and handling fees
baseCost += 15m;
// Currency exchange adjustment (simplified)
if (RequiresCurrencyConversion(destination))
{
baseCost *= 1.03m; // 3% exchange fee
}
return Math.Round(baseCost, 2);
}
public TimeSpan EstimateDelivery(decimal distance)
{
var days = (int)Math.Ceiling(distance / 200); // 200 km per day
return TimeSpan.FromDays(Math.Max(days, 7)); // Minimum 7 days for international
}
public bool IsAvailable(string destination)
{
return !IsRestrictedDestination(destination) &&
IsInternationalDestination(destination);
}
private bool RequiresCurrencyConversion(string destination)
{
var nonUsdDestinations = new[] { "europe", "uk", "canada", "australia", "japan" };
return nonUsdDestinations.Any(d => destination.ToLower().Contains(d));
}
private bool IsInternationalDestination(string destination)
{
var usDestinations = new[] { "usa", "united states", "us" };
return !usDestinations.Any(d => destination.ToLower().Contains(d));
}
private bool IsRestrictedDestination(string destination)
{
var restrictedDestinations = new[] { "cuba", "north korea", "syria", "iran", "russia" };
return restrictedDestinations.Any(d => destination.ToLower().Contains(d));
}
}}
Step 3: Strategy Context and Factory
// Patterns/Strategies/ShippingContext.csnamespace ECommercePatterns.Patterns.Strategies{
/// <summary>
/// Shipping context that uses the strategy pattern
/// </summary>
public interface IShippingCalculator
{
ShippingResult CalculateShipping(ShippingContext context, string method);
IEnumerable<ShippingOption> GetAvailableOptions(ShippingContext context);
IShippingStrategy GetStrategy(string method);
}
public class ShippingCalculator : IShippingCalculator
{
private readonly Dictionary<string, IShippingStrategy> _strategies;
private readonly ILogger<ShippingCalculator> _logger;
public ShippingCalculator(ILogger<ShippingCalculator> logger)
{
_logger = logger;
_strategies = new Dictionary<string, IShippingStrategy>
{
["standard"] = new StandardShippingStrategy(),
["express"] = new ExpressShippingStrategy(),
["overnight"] = new OvernightShippingStrategy(),
["international"] = new InternationalShippingStrategy()
};
}
public ShippingResult CalculateShipping(ShippingContext context, string method)
{
_logger.LogInformation("Calculating shipping for method: {Method}", method);
if (!_strategies.TryGetValue(method.ToLower(), out var strategy))
{
throw new ArgumentException($"Unknown shipping method: {method}");
}
if (!strategy.IsAvailable(context.Destination))
{
return new ShippingResult
{
IsAvailable = false,
Message = $"Shipping method '{method}' is not available for {context.Destination}"
};
}
var cost = strategy.CalculateCost(context.Weight, context.Distance, context.Destination);
var deliveryEstimate = strategy.EstimateDelivery(context.Distance);
_logger.LogDebug("Shipping calculated: {Cost} for {DeliveryEstimate}", cost, deliveryEstimate);
return new ShippingResult
{
Cost = cost,
EstimatedDelivery = deliveryEstimate,
Method = strategy.Name,
IsAvailable = true,
Message = $"{strategy.Name} shipping available"
};
}
public IEnumerable<ShippingOption> GetAvailableOptions(ShippingContext context)
{
var options = new List<ShippingOption>();
foreach (var strategy in _strategies.Values)
{
if (strategy.IsAvailable(context.Destination))
{
var cost = strategy.CalculateCost(context.Weight, context.Distance, context.Destination);
var deliveryEstimate = strategy.EstimateDelivery(context.Distance);
options.Add(new ShippingOption
{
Method = strategy.Name,
Cost = cost,
EstimatedDelivery = deliveryEstimate,
IsAvailable = true
});
}
}
_logger.LogDebug("Found {Count} available shipping options", options.Count);
return options.OrderBy(o => o.Cost);
}
public IShippingStrategy GetStrategy(string method)
{
return _strategies[method.ToLower()];
}
// Method to dynamically add new strategies at runtime
public void RegisterStrategy(string key, IShippingStrategy strategy)
{
_strategies[key.ToLower()] = strategy;
_logger.LogInformation("Registered new shipping strategy: {Key}", key);
}
}
/// <summary>
/// Shipping option DTO
/// </summary>
public class ShippingOption
{
public string Method { get; set; } = string.Empty;
public decimal Cost { get; set; }
public TimeSpan EstimatedDelivery { get; set; }
public bool IsAvailable { get; set; }
}}
Step 4: Usage in Services and Controllers
// Services/OrderShippingService.csnamespace ECommercePatterns.Services{
/// <summary>
/// Order shipping service using strategy pattern
/// </summary>
public interface IOrderShippingService
{
Task<ShippingSelectionResult> SelectBestShippingAsync(int orderId);
Task<decimal> CalculateShippingCostAsync(int orderId, string method);
Task<bool> ValidateShippingAsync(int orderId, string method);
}
public class OrderShippingService : IOrderShippingService
{
private readonly IUnitOfWork _unitOfWork;
private readonly IShippingCalculator _shippingCalculator;
private readonly ILogger<OrderShippingService> _logger;
public OrderShippingService(
IUnitOfWork unitOfWork,
IShippingCalculator shippingCalculator,
ILogger<OrderShippingService> logger)
{
_unitOfWork = unitOfWork;
_shippingCalculator = shippingCalculator;
_logger = logger;
}
public async Task<ShippingSelectionResult> SelectBestShippingAsync(int orderId)
{
_logger.LogInformation("Selecting best shipping for order {OrderId}", orderId);
var order = await _unitOfWork.Orders.GetOrderWithItemsAsync(orderId);
if (order == null)
{
throw new ArgumentException($"Order {orderId} not found");
}
var context = CreateShippingContext(order);
var availableOptions = _shippingCalculator.GetAvailableOptions(context);
var bestOption = availableOptions.FirstOrDefault();
if (bestOption == null)
{
return ShippingSelectionResult.Failure("No shipping options available");
}
_logger.LogInformation("Selected {Method} shipping for order {OrderId} at cost {Cost}",
bestOption.Method, orderId, bestOption.Cost);
return ShippingSelectionResult.Success(bestOption);
}
public async Task<decimal> CalculateShippingCostAsync(int orderId, string method)
{
var order = await _unitOfWork.Orders.GetOrderWithItemsAsync(orderId);
if (order == null)
{
throw new ArgumentException($"Order {orderId} not found");
}
var context = CreateShippingContext(order);
var result = _shippingCalculator.CalculateShipping(context, method);
if (!result.IsAvailable)
{
throw new InvalidOperationException(result.Message);
}
return result.Cost;
}
public async Task<bool> ValidateShippingAsync(int orderId, string method)
{
var order = await _unitOfWork.Orders.GetOrderWithItemsAsync(orderId);
if (order == null) return false;
var context = CreateShippingContext(order);
var result = _shippingCalculator.CalculateShipping(context, method);
return result.IsAvailable;
}
private ShippingContext CreateShippingContext(Order order)
{
var totalWeight = order.Items.Sum(i => i.Quantity * 0.5m); // Assume 0.5kg per item
var distance = CalculateDistance(order.ShippingAddress); // Simplified
return new ShippingContext
{
Weight = totalWeight,
Distance = distance,
Destination = order.ShippingAddress.City,
OrderDate = order.CreatedAt
};
}
private decimal CalculateDistance(Address address)
{
// Simplified distance calculation
// In real application, use geolocation services
return address.City.ToLower() switch
{
"new york" => 400,
"los angeles" => 3800,
"chicago" => 800,
"miami" => 1300,
_ => 1000 // Default distance
};
}
}
/// <summary>
/// Shipping selection result
/// </summary>
public class ShippingSelectionResult
{
public bool Success { get; }
public ShippingOption? SelectedOption { get; }
public string Message { get; }
private ShippingSelectionResult(bool success, ShippingOption? option, string message)
{
Success = success;
SelectedOption = option;
Message = message;
}
public static ShippingSelectionResult Success(ShippingOption option)
=> new ShippingSelectionResult(true, option, "Shipping option selected");
public static ShippingSelectionResult Failure(string message)
=> new ShippingSelectionResult(false, null, message);
}}
7. Decorator Pattern: Behavior Extension
The Problem: Rigid Inheritance Hierarchies
Without the Decorator pattern, you end up with complex inheritance trees:
// β Problem: Complex inheritance for cross-cutting concernspublic interface INotificationService{
Task SendNotificationAsync(string message, string recipient);}
public class EmailNotificationService : INotificationService{
public async Task SendNotificationAsync(string message, string recipient)
{
// Send email logic
}}
public class LoggingEmailNotificationService : EmailNotificationService{
private readonly ILogger _logger;
public override async Task SendNotificationAsync(string message, string recipient)
{
_logger.LogInformation("Sending notification to {Recipient}", recipient);
await base.SendNotificationAsync(message, recipient);
_logger.LogInformation("Notification sent to {Recipient}", recipient);
}}
public class RetryEmailNotificationService : EmailNotificationService{
public override async Task SendNotificationAsync(string message, string recipient)
{
for (int i = 0; i < 3; i++)
{
try
{
await base.SendNotificationAsync(message, recipient);
return;
}
catch
{
if (i == 2) throw;
await Task.Delay(1000);
}
}
}}
// How to combine logging AND retry? More inheritance? π±
Decorator Pattern Solution
The Decorator pattern attaches additional responsibilities to an object dynamically, providing a flexible alternative to subclassing for extending functionality.
Step 1: Base Interface and Implementation
// Patterns/Decorators/INotificationService.csnamespace ECommercePatterns.Patterns.Decorators{
/// <summary>
/// Notification service interface
/// </summary>
public interface INotificationService
{
Task<NotificationResult> SendAsync(string message, string recipient, string subject);
Task<bool> CanSendAsync(string recipient);
}
/// <summary>
/// Notification result
/// </summary>
public class NotificationResult
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public DateTime SentAt { get; set; }
public string NotificationId { get; set; } = Guid.NewGuid().ToString();
}
/// <summary>
/// Base email notification service
/// </summary>
public class EmailNotificationService : INotificationService
{
private readonly ILogger<EmailNotificationService> _logger;
public EmailNotificationService(ILogger<EmailNotificationService> logger)
{
_logger = logger;
}
public virtual async Task<NotificationResult> SendAsync(string message, string recipient, string subject)
{
_logger.LogInformation("Sending email to {Recipient} with subject: {Subject}", recipient, subject);
// Simulate email sending
await Task.Delay(500);
// Simulate occasional failures
if (DateTime.UtcNow.Second % 10 == 0) // Fail 10% of the time
{
throw new InvalidOperationException("Email service temporarily unavailable");
}
_logger.LogInformation("Email sent successfully to {Recipient}", recipient);
return new NotificationResult
{
Success = true,
Message = "Email sent successfully",
SentAt = DateTime.UtcNow
};
}
public virtual async Task<bool> CanSendAsync(string recipient)
{
// Basic validation
await Task.Delay(50);
return !string.IsNullOrEmpty(recipient) && recipient.Contains('@');
}
}}
Step 2: Base Decorator Class
// Patterns/Decorators/NotificationDecorator.csnamespace ECommercePatterns.Patterns.Decorators{
/// <summary>
/// Base decorator class for notification services
/// </summary>
public abstract class NotificationDecorator : INotificationService
{
protected readonly INotificationService _decoratedService;
protected readonly ILogger _logger;
protected NotificationDecorator(INotificationService decoratedService, ILogger logger)
{
_decoratedService = decoratedService;
_logger = logger;
}
public virtual async Task<NotificationResult> SendAsync(string message, string recipient, string subject)
{
return await _decoratedService.SendAsync(message, recipient, subject);
}
public virtual async Task<bool> CanSendAsync(string recipient)
{
return await _decoratedService.CanSendAsync(recipient);
}
}}
Step 3: Concrete Decorators
// Patterns/Decorators/ConcreteDecorators.csnamespace ECommercePatterns.Patterns.Decorators{
/// <summary>
/// Logging decorator for notifications
/// </summary>
public class LoggingNotificationDecorator : NotificationDecorator
{
public LoggingNotificationDecorator(
INotificationService decoratedService,
ILogger<LoggingNotificationDecorator> logger)
: base(decoratedService, logger)
{
}
public override async Task<NotificationResult> SendAsync(string message, string recipient, string subject)
{
_logger.LogInformation("Starting notification send to {Recipient}", recipient);
_logger.LogDebug("Message: {Message}, Subject: {Subject}", message, subject);
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
var result = await base.SendAsync(message, recipient, subject);
stopwatch.Stop();
_logger.LogInformation(
"Notification completed in {ElapsedMs}ms. Success: {Success}",
stopwatch.ElapsedMilliseconds, result.Success);
return result;
}
catch (Exception ex)
{
stopwatch.Stop();
_logger.LogError(
ex, "Notification failed after {ElapsedMs}ms for {Recipient}",
stopwatch.ElapsedMilliseconds, recipient);
throw;
}
}
}
/// <summary>
/// Retry decorator for notifications
/// </summary>
public class RetryNotificationDecorator : NotificationDecorator
{
private readonly int _maxRetries;
private readonly TimeSpan _retryDelay;
public RetryNotificationDecorator(
INotificationService decoratedService,
ILogger<RetryNotificationDecorator> logger,
int maxRetries = 3,
TimeSpan? retryDelay = null)
: base(decoratedService, logger)
{
_maxRetries = maxRetries;
_retryDelay = retryDelay ?? TimeSpan.FromSeconds(1);
}
public override async Task<NotificationResult> SendAsync(string message, string recipient, string subject)
{
var lastException = new Exception("Max retries exceeded");
for (int attempt = 1; attempt <= _maxRetries; attempt++)
{
try
{
_logger.LogDebug("Notification attempt {Attempt} of {MaxRetries}", attempt, _maxRetries);
return await base.SendAsync(message, recipient, subject);
}
catch (Exception ex)
{
lastException = ex;
if (attempt < _maxRetries)
{
_logger.LogWarning(
ex, "Notification attempt {Attempt} failed. Retrying in {DelayMs}ms",
attempt, _retryDelay.TotalMilliseconds);
await Task.Delay(_retryDelay);
}
}
}
_logger.LogError(lastException, "All {MaxRetries} notification attempts failed", _maxRetries);
throw lastException;
}
}
/// <summary>
/// Caching decorator for notification results
/// </summary>
public class CachingNotificationDecorator : NotificationDecorator
{
private readonly ICacheService _cacheService;
private readonly TimeSpan _cacheDuration;
public CachingNotificationDecorator(
INotificationService decoratedService,
ICacheService cacheService,
ILogger<CachingNotificationDecorator> logger,
TimeSpan? cacheDuration = null)
: base(decoratedService, logger)
{
_cacheService = cacheService;
_cacheDuration = cacheDuration ?? TimeSpan.FromMinutes(30);
}
public override async Task<NotificationResult> SendAsync(string message, string recipient, string subject)
{
var cacheKey = $"notification_{recipient}_{subject.GetHashCode()}";
// Check cache first
var cachedResult = await _cacheService.GetAsync<NotificationResult>(cacheKey);
if (cachedResult != null)
{
_logger.LogDebug("Returning cached notification result for {Recipient}", recipient);
return cachedResult;
}
// Send notification and cache result
var result = await base.SendAsync(message, recipient, subject);
if (result.Success)
{
await _cacheService.SetAsync(cacheKey, result, _cacheDuration);
_logger.LogDebug("Cached notification result for {Recipient}", recipient);
}
return result;
}
}
/// <summary>
/// Validation decorator for notifications
/// </summary>
public class ValidationNotificationDecorator : NotificationDecorator
{
public ValidationNotificationDecorator(
INotificationService decoratedService,
ILogger<ValidationNotificationDecorator> logger)
: base(decoratedService, logger)
{
}
public override async Task<NotificationResult> SendAsync(string message, string recipient, string subject)
{
// Validate inputs
if (string.IsNullOrWhiteSpace(message))
{
throw new ArgumentException("Message cannot be empty", nameof(message));
}
if (string.IsNullOrWhiteSpace(recipient))
{
throw new ArgumentException("Recipient cannot be empty", nameof(recipient));
}
if (string.IsNullOrWhiteSpace(subject))
{
throw new ArgumentException("Subject cannot be empty", nameof(subject));
}
if (message.Length > 1000)
{
throw new ArgumentException("Message too long", nameof(message));
}
_logger.LogDebug("Notification validation passed for {Recipient}", recipient);
return await base.SendAsync(message, recipient, subject);
}
public override async Task<bool> CanSendAsync(string recipient)
{
if (string.IsNullOrWhiteSpace(recipient) || !recipient.Contains('@'))
{
return false;
}
return await base.CanSendAsync(recipient);
}
}
/// <summary>
/// Rate limiting decorator for notifications
/// </summary>
public class RateLimitingNotificationDecorator : NotificationDecorator
{
private readonly SemaphoreSlim _semaphore;
private readonly int _maxConcurrent;
public RateLimitingNotificationDecorator(
INotificationService decoratedService,
ILogger<RateLimitingNotificationDecorator> logger,
int maxConcurrent = 5)
: base(decoratedService, logger)
{
_maxConcurrent = maxConcurrent;
_semaphore = new SemaphoreSlim(maxConcurrent, maxConcurrent);
}
public override async Task<NotificationResult> SendAsync(string message, string recipient, string subject)
{
await _semaphore.WaitAsync();
try
{
_logger.LogDebug(
"Acquired rate limit slot. Available: {Available}",
_semaphore.CurrentCount);
return await base.SendAsync(message, recipient, subject);
}
finally
{
_semaphore.Release();
_logger.LogDebug("Released rate limit slot. Available: {Available}", _semaphore.CurrentCount);
}
}
}}
Step 4: Decorator Factory and Registration
// Patterns/Decorators/NotificationServiceFactory.csnamespace ECommercePatterns.Patterns.Decorators{
/// <summary>
/// Factory for creating decorated notification services
/// </summary>
public interface INotificationServiceFactory
{
INotificationService CreateService(NotificationServiceType type);
INotificationService CreateServiceWithAllDecorators();
}
public class NotificationServiceFactory : INotificationServiceFactory
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<NotificationServiceFactory> _logger;
public NotificationServiceFactory(IServiceProvider serviceProvider, ILogger<NotificationServiceFactory> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public INotificationService CreateService(NotificationServiceType type)
{
_logger.LogInformation("Creating notification service of type: {Type}", type);
var baseService = new EmailNotificationService(
_serviceProvider.GetRequiredService<ILogger<EmailNotificationService>>());
return type switch
{
NotificationServiceType.Basic => baseService,
NotificationServiceType.WithLogging => new LoggingNotificationDecorator(
baseService,
_serviceProvider.GetRequiredService<ILogger<LoggingNotificationDecorator>>()),
NotificationServiceType.WithRetry => new RetryNotificationDecorator(
baseService,
_serviceProvider.GetRequiredService<ILogger<RetryNotificationDecorator>>()),
NotificationServiceType.WithCaching => new CachingNotificationDecorator(
baseService,
_serviceProvider.GetRequiredService<ICacheService>(),
_serviceProvider.GetRequiredService<ILogger<CachingNotificationDecorator>>()),
NotificationServiceType.FullFeatured => CreateFullFeaturedService(baseService),
_ => throw new ArgumentException($"Unknown service type: {type}")
};
}
public INotificationService CreateServiceWithAllDecorators()
{
_logger.LogInformation("Creating fully decorated notification service");
var baseService = new EmailNotificationService(
_serviceProvider.GetRequiredService<ILogger<EmailNotificationService>>());
return CreateFullFeaturedService(baseService);
}
private INotificationService CreateFullFeaturedService(INotificationService baseService)
{
// Apply decorators in specific order
var service = new ValidationNotificationDecorator(
baseService,
_serviceProvider.GetRequiredService<ILogger<ValidationNotificationDecorator>>());
service = new LoggingNotificationDecorator(
service,
_serviceProvider.GetRequiredService<ILogger<LoggingNotificationDecorator>>());
service = new RetryNotificationDecorator(
service,
_serviceProvider.GetRequiredService<ILogger<RetryNotificationDecorator>>());
service = new RateLimitingNotificationDecorator(
service,
_serviceProvider.GetRequiredService<ILogger<RateLimitingNotificationDecorator>>());
service = new CachingNotificationDecorator(
service,
_serviceProvider.GetRequiredService<ICacheService>(),
_serviceProvider.GetRequiredService<ILogger<CachingNotificationDecorator>>());
return service;
}
}
/// <summary>
/// Notification service types
/// </summary>
public enum NotificationServiceType
{
Basic,
WithLogging,
WithRetry,
WithCaching,
FullFeatured
}}
Step 5: Usage Example
// Services/OrderNotificationService.csnamespace ECommercePatterns.Services{
/// <summary>
/// Order notification service using decorated notification services
/// </summary>
public interface IOrderNotificationService
{
Task NotifyOrderConfirmationAsync(int orderId);
Task NotifyOrderShippedAsync(int orderId);
Task NotifyOrderDeliveredAsync(int orderId);
}
public class OrderNotificationService : IOrderNotificationService
{
private readonly IUnitOfWork _unitOfWork;
private readonly INotificationServiceFactory _notificationFactory;
private readonly ILogger<OrderNotificationService> _logger;
public OrderNotificationService(
IUnitOfWork unitOfWork,
INotificationServiceFactory notificationFactory,
ILogger<OrderNotificationService> logger)
{
_unitOfWork = unitOfWork;
_notificationFactory = notificationFactory;
_logger = logger;
}
public async Task NotifyOrderConfirmationAsync(int orderId)
{
_logger.LogInformation("Sending order confirmation for order {OrderId}", orderId);
var order = await _unitOfWork.Orders.GetOrderWithItemsAsync(orderId);
if (order == null)
{
throw new ArgumentException($"Order {orderId} not found");
}
var user = await _unitOfWork.Users.GetByIdAsync(order.UserId);
if (user == null)
{
throw new ArgumentException($"User for order {orderId} not found");
}
var notificationService = _notificationFactory.CreateService(NotificationServiceType.FullFeatured);
var message = CreateOrderConfirmationMessage(order);
var subject = $"Order Confirmation - #{order.Id}";
await notificationService.SendAsync(message, user.Email, subject);
}
public async Task NotifyOrderShippedAsync(int orderId)
{
// Similar implementation for shipped notifications
await Task.CompletedTask;
}
public async Task NotifyOrderDeliveredAsync(int orderId)
{
// Similar implementation for delivered notifications
await Task.CompletedTask;
}
private string CreateOrderConfirmationMessage(Order order)
{
return $"""
Thank you for your order!
Order Details:
Order Number: #{order.Id}
Order Date: {order.CreatedAt:MMMM dd, yyyy}
Total Amount: {order.TotalAmount:C}
Items:
{string.Join("\n", order.Items.Select(i => $"- {i.Quantity}x {i.Product?.Name} @ {i.UnitPrice:C}"))}
Shipping Address:
{order.ShippingAddress.Street}
{order.ShippingAddress.City}, {order.ShippingAddress.State} {order.ShippingAddress.ZipCode}
We'll notify you when your order ships!
""";
}
}}
8. Observer Pattern: Event-Driven Architecture
The Problem: Tight Coupling in Event Handling
Without the Observer pattern, event handling becomes tightly coupled:
// β Problem: Tight coupling between event publishers and subscriberspublic class OrderService{
private readonly IEmailService _emailService;
private readonly IInventoryService _inventoryService;
private readonly IAnalyticsService _analyticsService;
private readonly INotificationService _notificationService;
public async Task PlaceOrder(Order order)
{
// Process order logic...
// Notify all interested parties directly
await _emailService.SendOrderConfirmation(order);
await _inventoryService.UpdateStock(order);
await _analyticsService.RecordOrder(order);
await _notificationService.SendPushNotification(order);
// Adding new subscribers requires modifying this class
}}
Observer Pattern Solution
The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
Step 1: Event Definitions
// Patterns/Observers/Events.csnamespace ECommercePatterns.Patterns.Observers{
/// <summary>
/// Base event interface
/// </summary>
public interface IEvent
{
DateTime OccurredAt { get; }
string EventType { get; }
}
/// <summary>
/// Order created event
/// </summary>
public class OrderCreatedEvent : IEvent
{
public DateTime OccurredAt { get; } = DateTime.UtcNow;
public string EventType => "OrderCreated";
public int OrderId { get; }
public int UserId { get; }
public decimal TotalAmount { get; }
public DateTime OrderDate { get; }
public OrderCreatedEvent(int orderId, int userId, decimal totalAmount, DateTime orderDate)
{
OrderId = orderId;
UserId = userId;
TotalAmount = totalAmount;
OrderDate = orderDate;
}
}
/// <summary>
/// Order shipped event
/// </summary>
public class OrderShippedEvent : IEvent
{
public DateTime OccurredAt { get; } = DateTime.UtcNow;
public string EventType => "OrderShipped";
public int OrderId { get; }
public string TrackingNumber { get; }
public DateTime ShippedDate { get; }
public OrderShippedEvent(int orderId, string trackingNumber, DateTime shippedDate)
{
OrderId = orderId;
TrackingNumber = trackingNumber;
ShippedDate = shippedDate;
}
}
/// <summary>
/// Order delivered event
/// </summary>
public class OrderDeliveredEvent : IEvent
{
public DateTime OccurredAt { get; } = DateTime.UtcNow;
public string EventType => "OrderDelivered";
public int OrderId { get; }
public DateTime DeliveredDate { get; }
public OrderDeliveredEvent(int orderId, DateTime deliveredDate)
{
OrderId = orderId;
DeliveredDate = deliveredDate;
}
}
/// <summary>
/// Payment received event
/// </summary>
public class PaymentReceivedEvent : IEvent
{
public DateTime OccurredAt { get; } = DateTime.UtcNow;
public string EventType => "PaymentReceived";
public int OrderId { get; }
public decimal Amount { get; }
public string PaymentMethod { get; }
public string TransactionId { get; }
public PaymentReceivedEvent(int orderId, decimal amount, string paymentMethod, string transactionId)
{
OrderId = orderId;
Amount = amount;
PaymentMethod = paymentMethod;
TransactionId = transactionId;
}
}
/// <summary>
/// Product stock updated event
/// </summary>
public class ProductStockUpdatedEvent : IEvent
{
public DateTime OccurredAt { get; } = DateTime.UtcNow;
public string EventType => "ProductStockUpdated";
public int ProductId { get; }
public int OldStock { get; }
public int NewStock { get; }
public string Reason { get; }
public ProductStockUpdatedEvent(int productId, int oldStock, int newStock, string reason)
{
ProductId = productId;
OldStock = oldStock;
NewStock = newStock;
Reason = reason;
}
}}
Step 2: Event Handler Interface
// Patterns/Observers/IEventHandler.csnamespace ECommercePatterns.Patterns.Observers{
/// <summary>
/// Event handler interface
/// </summary>
/// <typeparam name="TEvent">Event type</typeparam>
public interface IEventHandler<TEvent> where TEvent : IEvent
{
Task HandleAsync(TEvent @event);
}
/// <summary>
/// Event bus for publishing events to handlers
/// </summary>
public interface IEventBus
{
Task PublishAsync<TEvent>(TEvent @event) where TEvent : IEvent;
void Subscribe<TEvent>(IEventHandler<TEvent> handler) where TEvent : IEvent;
void Unsubscribe<TEvent>(IEventHandler<TEvent> handler) where TEvent : IEvent;
}}
Step 3: Concrete Event Handlers
// Patterns/Observers/EventHandlers.csnamespace ECommercePatterns.Patterns.Observers{
/// <summary>
/// Email notification handler for order events
/// </summary>
public class OrderEmailNotificationHandler : IEventHandler<OrderCreatedEvent>,
IEventHandler<OrderShippedEvent>,
IEventHandler<OrderDeliveredEvent>
{
private readonly INotificationService _notificationService;
private readonly IUserRepository _userRepository;
private readonly ILogger<OrderEmailNotificationHandler> _logger;
public OrderEmailNotificationHandler(
INotificationService notificationService,
IUserRepository userRepository,
ILogger<OrderEmailNotificationHandler> logger)
{
_notificationService = notificationService;
_userRepository = userRepository;
_logger = logger;
}
public async Task HandleAsync(OrderCreatedEvent @event)
{
_logger.LogInformation("Sending order confirmation email for order {OrderId}", @event.OrderId);
var user = await _userRepository.GetByIdAsync(@event.UserId);
if (user == null)
{
_logger.LogWarning("User {UserId} not found for order {OrderId}", @event.UserId, @event.OrderId);
return;
}
var message = $"Your order #{@event.OrderId} has been confirmed. Total: {@event.TotalAmount:C}";
var subject = $"Order Confirmation - #{@event.OrderId}";
await _notificationService.SendAsync(message, user.Email, subject);
_logger.LogInformation("Order confirmation email sent for order {OrderId}", @event.OrderId);
}
public async Task HandleAsync(OrderShippedEvent @event)
{
_logger.LogInformation("Sending shipment notification for order {OrderId}", @event.OrderId);
// Similar implementation for shipped notifications
await Task.CompletedTask;
}
public async Task HandleAsync(OrderDeliveredEvent @event)
{
_logger.LogInformation("Sending delivery notification for order {OrderId}", @event.OrderId);
// Similar implementation for delivered notifications
await Task.CompletedTask;
}
}
/// <summary>
/// Inventory management handler for order events
/// </summary>
public class InventoryManagementHandler : IEventHandler<OrderCreatedEvent>,
IEventHandler<PaymentReceivedEvent>
{
private readonly IProductRepository _productRepository;
private readonly IOrderRepository _orderRepository;
private readonly ILogger<InventoryManagementHandler> _logger;
public InventoryManagementHandler(
IProductRepository productRepository,
IOrderRepository orderRepository,
ILogger<InventoryManagementHandler> logger)
{
_productRepository = productRepository;
_orderRepository = orderRepository;
_logger = logger;
}
public async Task HandleAsync(OrderCreatedEvent @event)
{
_logger.LogInformation("Reserving inventory for order {OrderId}", @event.OrderId);
var order = await _orderRepository.GetOrderWithItemsAsync(@event.OrderId);
if (order == null)
{
_logger.LogWarning("Order {OrderId} not found for inventory reservation", @event.OrderId);
return;
}
foreach (var item in order.Items)
{
try
{
await _productRepository.UpdateStockAsync(item.ProductId, -item.Quantity);
_logger.LogDebug("Reserved {Quantity} units of product {ProductId}",
item.Quantity, item.ProductId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to reserve inventory for product {ProductId}", item.ProductId);
// In real application, you might want to compensate or retry
}
}
_logger.LogInformation("Inventory reservation completed for order {OrderId}", @event.OrderId);
}
public async Task HandleAsync(PaymentReceivedEvent @event)
{
_logger.LogInformation("Processing inventory allocation for paid order {OrderId}", @event.OrderId);
// When payment is received, permanently allocate inventory
// This might involve updating inventory records, generating pick lists, etc.
await Task.CompletedTask;
}
}
/// <summary>
/// Analytics handler for various events
/// </summary>
public class AnalyticsEventHandler : IEventHandler<OrderCreatedEvent>,
IEventHandler<OrderShippedEvent>,
IEventHandler<OrderDeliveredEvent>,
IEventHandler<PaymentReceivedEvent>
{
private readonly IAnalyticsService _analyticsService;
private readonly ILogger<AnalyticsEventHandler> _logger;
public AnalyticsEventHandler(IAnalyticsService analyticsService, ILogger<AnalyticsEventHandler> logger)
{
_analyticsService = analyticsService;
_logger = logger;
}
public async Task HandleAsync(OrderCreatedEvent @event)
{
_logger.LogDebug("Recording order creation in analytics for order {OrderId}", @event.OrderId);
await _analyticsService.RecordEventAsync("order_created", new
{
order_id = @event.OrderId,
user_id = @event.UserId,
total_amount = @event.TotalAmount,
order_date = @event.OrderDate
});
}
public async Task HandleAsync(OrderShippedEvent @event)
{
_logger.LogDebug("Recording order shipment in analytics for order {OrderId}", @event.OrderId);
await _analyticsService.RecordEventAsync("order_shipped", new
{
order_id = @event.OrderId,
shipped_date = @event.ShippedDate,
tracking_number = @event.TrackingNumber
});
}
public async Task HandleAsync(OrderDeliveredEvent @event)
{
_logger.LogDebug("Recording order delivery in analytics for order {OrderId}", @event.OrderId);
await _analyticsService.RecordEventAsync("order_delivered", new
{
order_id = @event.OrderId,
delivered_date = @event.DeliveredDate
});
}
public async Task HandleAsync(PaymentReceivedEvent @event)
{
_logger.LogDebug("Recording payment in analytics for order {OrderId}", @event.OrderId);
await _analyticsService.RecordEventAsync("payment_received", new
{
order_id = @event.OrderId,
amount = @event.Amount,
payment_method = @event.PaymentMethod,
transaction_id = @event.TransactionId
});
}
}
/// <summary>
/// Fraud detection handler
/// </summary>
public class FraudDetectionHandler : IEventHandler<OrderCreatedEvent>,
IEventHandler<PaymentReceivedEvent>
{
private readonly IFraudDetectionService _fraudService;
private readonly IOrderRepository _orderRepository;
private readonly ILogger<FraudDetectionHandler> _logger;
public FraudDetectionHandler(
IFraudDetectionService fraudService,
IOrderRepository orderRepository,
ILogger<FraudDetectionHandler> logger)
{
_fraudService = fraudService;
_orderRepository = orderRepository;
_logger = logger;
}
public async Task HandleAsync(OrderCreatedEvent @event)
{
_logger.LogInformation("Running fraud check for order {OrderId}", @event.OrderId);
var order = await _orderRepository.GetOrderWithItemsAsync(@event.OrderId);
if (order == null) return;
var isFraudulent = await _fraudService.CheckOrderAsync(order);
if (isFraudulent)
{
_logger.LogWarning("Order {OrderId} flagged as potentially fraudulent", @event.OrderId);
// In real application, you might update order status, notify admins, etc.
await _orderRepository.UpdateOrderStatusAsync(@event.OrderId, OrderStatus.UnderReview);
}
}
public async Task HandleAsync(PaymentReceivedEvent @event)
{
_logger.LogInformation("Running payment fraud check for order {OrderId}", @event.OrderId);
var isSuspicious = await _fraudService.CheckPaymentAsync(@event);
if (isSuspicious)
{
_logger.LogWarning("Payment for order {OrderId} flagged as suspicious", @event.OrderId);
// Additional fraud detection logic
}
}
}}
Step 4: Event Bus Implementation
// Patterns/Observers/EventBus.csnamespace ECommercePatterns.Patterns.Observers{
/// <summary>
/// In-memory event bus implementation
/// </summary>
public class InMemoryEventBus : IEventBus
{
private readonly ConcurrentDictionary<Type, List<object>> _handlers = new();
private readonly ILogger<InMemoryEventBus> _logger;
private readonly IServiceProvider _serviceProvider;
public InMemoryEventBus(ILogger<InMemoryEventBus> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}
public async Task PublishAsync<TEvent>(TEvent @event) where TEvent : IEvent
{
_logger.LogInformation("Publishing event: {EventType} {@Event}", @event.EventType, @event);
var eventType = typeof(TEvent);
if (_handlers.TryGetValue(eventType, out var handlers))
{
var tasks = handlers.Cast<IEventHandler<TEvent>>()
.Select(handler => HandleEventWithRetryAsync(handler, @event))
.ToArray();
await Task.WhenAll(tasks);
}
else
{
_logger.LogDebug("No handlers registered for event type: {EventType}", eventType.Name);
}
}
public void Subscribe<TEvent>(IEventHandler<TEvent> handler) where TEvent : IEvent
{
var eventType = typeof(TEvent);
var handlers = _handlers.GetOrAdd(eventType, _ => new List<object>());
lock (handlers)
{
if (!handlers.Contains(handler))
{
handlers.Add(handler);
_logger.LogDebug("Registered handler {HandlerType} for event {EventType}",
handler.GetType().Name, eventType.Name);
}
}
}
public void Unsubscribe<TEvent>(IEventHandler<TEvent> handler) where TEvent : IEvent
{
var eventType = typeof(TEvent);
if (_handlers.TryGetValue(eventType, out var handlers))
{
lock (handlers)
{
handlers.Remove(handler);
_logger.LogDebug("Unregistered handler {HandlerType} for event {EventType}",
handler.GetType().Name, eventType.Name);
}
}
}
private async Task HandleEventWithRetryAsync<TEvent>(IEventHandler<TEvent> handler, TEvent @event)
where TEvent : IEvent
{
const int maxRetries = 3;
var retryDelay = TimeSpan.FromSeconds(1);
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
await handler.HandleAsync(@event);
return;
}
catch (Exception ex)
{
_logger.LogError(ex,
"Error handling event {EventType} with handler {HandlerType} (attempt {Attempt})",
@event.EventType, handler.GetType().Name, attempt);
if (attempt == maxRetries)
{
_logger.LogError(ex,
"All {MaxRetries} attempts failed for event {EventType}",
maxRetries, @event.EventType);
throw;
}
await Task.Delay(retryDelay);
retryDelay = TimeSpan.FromSeconds(retryDelay.TotalSeconds * 2); // Exponential backoff
}
}
}
}
/// <summary>
/// Event bus with dependency injection support
/// </summary>
public class ServiceCollectionEventBus : IEventBus
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ServiceCollectionEventBus> _logger;
public ServiceCollectionEventBus(IServiceProvider serviceProvider, ILogger<ServiceCollectionEventBus> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public async Task PublishAsync<TEvent>(TEvent @event) where TEvent : IEvent
{
_logger.LogInformation("Publishing event: {EventType}", @event.EventType);
using var scope = _serviceProvider.CreateScope();
var handlers = scope.ServiceProvider.GetServices<IEventHandler<TEvent>>();
if (handlers.Any())
{
var tasks = handlers.Select(handler => handler.HandleAsync(@event)).ToArray();
await Task.WhenAll(tasks);
_logger.LogDebug("Event {EventType} handled by {HandlerCount} handlers",
@event.EventType, handlers.Count());
}
else
{
_logger.LogDebug("No handlers found for event type: {EventType}", typeof(TEvent).Name);
}
}
public void Subscribe<TEvent>(IEventHandler<TEvent> handler) where TEvent : IEvent
{
// With DI, subscription is automatic through service registration
_logger.LogDebug("Subscription not required with DI for event {EventType}", typeof(TEvent).Name);
}
public void Unsubscribe<TEvent>(IEventHandler<TEvent> handler) where TEvent : IEvent
{
// With DI, unsubscription is handled by service lifetime
_logger.LogDebug("Unsubscription not required with DI for event {EventType}", typeof(TEvent).Name);
}
}}
Step 5: Event Publishing Service
// Services/EventPublishingService.csnamespace ECommercePatterns.Services{
/// <summary>
/// Service for publishing domain events
/// </summary>
public interface IEventPublishingService
{
Task PublishOrderCreatedAsync(int orderId, int userId, decimal totalAmount);
Task PublishOrderShippedAsync(int orderId, string trackingNumber);
Task PublishOrderDeliveredAsync(int orderId);
Task PublishPaymentReceivedAsync(int orderId, decimal amount, string paymentMethod, string transactionId);
Task PublishProductStockUpdatedAsync(int productId, int oldStock, int newStock, string reason);
}
public class EventPublishingService : IEventPublishingService
{
private readonly IEventBus _eventBus;
private readonly ILogger<EventPublishingService> _logger;
public EventPublishingService(IEventBus eventBus, ILogger<EventPublishingService> logger)
{
_eventBus = eventBus;
_logger = logger;
}
public async Task PublishOrderCreatedAsync(int orderId, int userId, decimal totalAmount)
{
var @event = new OrderCreatedEvent(orderId, userId, totalAmount, DateTime.UtcNow);
await _eventBus.PublishAsync(@event);
}
public async Task PublishOrderShippedAsync(int orderId, string trackingNumber)
{
var @event = new OrderShippedEvent(orderId, trackingNumber, DateTime.UtcNow);
await _eventBus.PublishAsync(@event);
}
public async Task PublishOrderDeliveredAsync(int orderId)
{
var @event = new OrderDeliveredEvent(orderId, DateTime.UtcNow);
await _eventBus.PublishAsync(@event);
}
public async Task PublishPaymentReceivedAsync(int orderId, decimal amount, string paymentMethod, string transactionId)
{
var @event = new PaymentReceivedEvent(orderId, amount, paymentMethod, transactionId);
await _eventBus.PublishAsync(@event);
}
public async Task PublishProductStockUpdatedAsync(int productId, int oldStock, int newStock, string reason)
{
var @event = new ProductStockUpdatedEvent(productId, oldStock, newStock, reason);
await _eventBus.PublishAsync(@event);
}
}}
9. Mediator Pattern: Decoupled Communication
The Problem: Direct Component Dependencies
Without the Mediator pattern, components communicate directly, creating tight coupling:
// β Problem: Direct dependencies between componentspublic class OrderProcessingService{
private readonly IPaymentService _paymentService;
private readonly IInventoryService _inventoryService;
private readonly IShippingService _shippingService;
private readonly INotificationService _notificationService;
private readonly IAnalyticsService _analyticsService;
public async Task ProcessOrder(Order order)
{
// Process payment
var paymentResult = await _paymentService.ProcessPayment(order);
if (!paymentResult.Success) return;
// Update inventory
await _inventoryService.UpdateStock(order);
// Schedule shipping
await _shippingService.ScheduleShipping(order);
// Send notifications
await _notificationService.SendOrderConfirmation(order);
// Record analytics
await _analyticsService.RecordOrder(order);
// Components are tightly coupled and know about each other
}}
Mediator Pattern Solution
The Mediator pattern defines an object that encapsulates how a set of objects interact, promoting loose coupling by keeping objects from referring to each other explicitly.
Step 1: Mediator and Command Definitions
// Patterns/Mediators/IMediator.csnamespace ECommercePatterns.Patterns.Mediators{
/// <summary>
/// Mediator interface for handling commands and queries
/// </summary>
public interface IMediator
{
Task<TResponse> Send<TResponse>(ICommand<TResponse> command);
Task<TResponse> Send<TResponse>(IQuery<TResponse> query);
Task Publish<TNotification>(TNotification notification) where TNotification : INotification;
}
/// <summary>
/// Command interface
/// </summary>
/// <typeparam name="TResponse">Response type</typeparam>
public interface ICommand<out TResponse> { }
/// <summary>
/// Query interface
/// </summary>
/// <typeparam name="TResponse">Response type</typeparam>
public interface IQuery<out TResponse> { }
/// <summary>
/// Notification interface
/// </summary>
public interface INotification { }
/// <summary>
/// Command handler interface
/// </summary>
/// <typeparam name="TCommand">Command type</typeparam>
/// <typeparam name="TResponse">Response type</typeparam>
public interface ICommandHandler<TCommand, TResponse>
where TCommand : ICommand<TResponse>
{
Task<TResponse> Handle(TCommand command, CancellationToken cancellationToken);
}
/// <summary>
/// Query handler interface
/// </summary>
/// <typeparam name="TQuery">Query type</typeparam>
/// <typeparam name="TResponse">Response type</typeparam>
public interface IQueryHandler<TQuery, TResponse>
where TQuery : IQuery<TResponse>
{
Task<TResponse> Handle(TQuery query, CancellationToken cancellationToken);
}
/// <summary>
/// Notification handler interface
/// </summary>
/// <typeparam name="TNotification">Notification type</typeparam>
public interface INotificationHandler<TNotification>
where TNotification : INotification
{
Task Handle(TNotification notification, CancellationToken cancellationToken);
}}
Step 2: Command and Query Definitions
// Patterns/Mediators/CommandsAndQueries.csnamespace ECommercePatterns.Patterns.Mediators{
/// <summary>
/// Create order command
/// </summary>
public class CreateOrderCommand : ICommand<OrderResult>
{
public int UserId { get; }
public List<OrderItemRequest> Items { get; }
public Address ShippingAddress { get; }
public CreateOrderCommand(int userId, List<OrderItemRequest> items, Address shippingAddress)
{
UserId = userId;
Items = items;
ShippingAddress = shippingAddress;
}
}
/// <summary>
/// Cancel order command
/// </summary>
public class CancelOrderCommand : ICommand<bool>
{
public int OrderId { get; }
public string Reason { get; }
public CancelOrderCommand(int orderId, string reason)
{
OrderId = orderId;
Reason = reason;
}
}
/// <summary>
/// Process payment command
/// </summary>
public class ProcessPaymentCommand : ICommand<PaymentResult>
{
public int OrderId { get; }
public string PaymentMethod { get; }
public PaymentInfo PaymentInfo { get; }
public ProcessPaymentCommand(int orderId, string paymentMethod, PaymentInfo paymentInfo)
{
OrderId = orderId;
PaymentMethod = paymentMethod;
PaymentInfo = paymentInfo;
}
}
/// <summary>
/// Get order details query
/// </summary>
public class GetOrderDetailsQuery : IQuery<OrderDetailsResult>
{
public int OrderId { get; }
public GetOrderDetailsQuery(int orderId)
{
OrderId = orderId;
}
}
/// <summary>
/// Get user orders query
/// </summary>
public class GetUserOrdersQuery : IQuery<UserOrdersResult>
{
public int UserId { get; }
public int Page { get; }
public int PageSize { get; }
public GetUserOrdersQuery(int userId, int page = 1, int pageSize = 10)
{
UserId = userId;
Page = page;
PageSize = pageSize;
}
}
/// <summary>
/// Order created notification
/// </summary>
public class OrderCreatedNotification : INotification
{
public int OrderId { get; }
public int UserId { get; }
public decimal TotalAmount { get; }
public OrderCreatedNotification(int orderId, int userId, decimal totalAmount)
{
OrderId = orderId;
UserId = userId;
TotalAmount = totalAmount;
}
}
/// <summary>
/// Payment processed notification
/// </summary>
public class PaymentProcessedNotification : INotification
{
public int OrderId { get; }
public bool Success { get; }
public string TransactionId { get; }
public PaymentProcessedNotification(int orderId, bool success, string transactionId)
{
OrderId = orderId;
Success = success;
TransactionId = transactionId;
}
}}
Step 3: Result Classes
// Patterns/Mediators/Results.csnamespace ECommercePatterns.Patterns.Mediators{
/// <summary>
/// Order operation result
/// </summary>
public class OrderResult
{
public bool Success { get; }
public int OrderId { get; }
public decimal TotalAmount { get; }
public string Message { get; }
public List<string> Errors { get; }
private OrderResult(bool success, int orderId, decimal totalAmount, string message, List<string> errors)
{
Success = success;
OrderId = orderId;
TotalAmount = totalAmount;
Message = message;
Errors = errors;
}
public static OrderResult SuccessResult(int orderId, decimal totalAmount, string message = "Order created successfully")
=> new OrderResult(true, orderId, totalAmount, message, new List<string>());
public static OrderResult Failure(string message, List<string> errors)
=> new OrderResult(false, 0, 0, message, errors);
public static OrderResult Failure(string message)
=> new OrderResult(false, 0, 0, message, new List<string> { message });
}
/// <summary>
/// Order details result
/// </summary>
public class OrderDetailsResult
{
public bool Found { get; }
public Order? Order { get; }
public string Message { get; }
private OrderDetailsResult(bool found, Order? order, string message)
{
Found = found;
Order = order;
Message = message;
}
public static OrderDetailsResult Success(Order order)
=> new OrderDetailsResult(true, order, "Order found");
public static OrderDetailsResult NotFound(int orderId)
=> new OrderDetailsResult(false, null, $"Order {orderId} not found");
}
/// <summary>
/// User orders result
/// </summary>
public class UserOrdersResult
{
public List<Order> Orders { get; }
public int TotalCount { get; }
public int Page { get; }
public int PageSize { get; }
public int TotalPages { get; }
public UserOrdersResult(List<Order> orders, int totalCount, int page, int pageSize)
{
Orders = orders;
TotalCount = totalCount;
Page = page;
PageSize = pageSize;
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize);
}
}}
Step 4: Command and Query Handlers
// Patterns/Mediators/Handlers.csnamespace ECommercePatterns.Patterns.Mediators{
/// <summary>
/// Create order command handler
/// </summary>
public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand, OrderResult>
{
private readonly IUnitOfWork _unitOfWork;
private readonly IMediator _mediator;
private readonly ILogger<CreateOrderCommandHandler> _logger;
public CreateOrderCommandHandler(
IUnitOfWork unitOfWork,
IMediator mediator,
ILogger<CreateOrderCommandHandler> logger)
{
_unitOfWork = unitOfWork;
_mediator = mediator;
_logger = logger;
}
public async Task<OrderResult> Handle(CreateOrderCommand command, CancellationToken cancellationToken)
{
_logger.LogInformation("Creating order for user {UserId}", command.UserId);
// Validate order items
var validationResult = await ValidateOrderItemsAsync(command.Items);
if (!validationResult.IsValid)
{
return OrderResult.Failure("Order validation failed", validationResult.Errors);
}
await _unitOfWork.BeginTransactionAsync();
try
{
// Create order
var order = new Order
{
UserId = command.UserId,
Status = OrderStatus.Pending,
TotalAmount = await CalculateOrderTotalAsync(command.Items),
ShippingAddress = command.ShippingAddress,
Items = command.Items.Select(item => new OrderItem
{
ProductId = item.ProductId,
Quantity = item.Quantity,
UnitPrice = await GetProductPriceAsync(item.ProductId)
}).ToList()
};
// Reserve inventory
foreach (var item in command.Items)
{
await _unitOfWork.Products.UpdateStockAsync(item.ProductId, -item.Quantity);
}
// Save order
await _unitOfWork.Orders.AddAsync(order);
await _unitOfWork.CommitTransactionAsync();
// Publish order created notification
await _mediator.Publish(new OrderCreatedNotification(order.Id, order.UserId, order.TotalAmount));
_logger.LogInformation("Order {OrderId} created successfully", order.Id);
return OrderResult.SuccessResult(order.Id, order.TotalAmount);
}
catch (Exception ex)
{
await _unitOfWork.RollbackTransactionAsync();
_logger.LogError(ex, "Error creating order for user {UserId}", command.UserId);
return OrderResult.Failure("An error occurred while creating the order");
}
}
private async Task<ValidationResult> ValidateOrderItemsAsync(List<OrderItemRequest> items)
{
var errors = new List<string>();
foreach (var item in items)
{
var product = await _unitOfWork.Products.GetByIdAsync(item.ProductId);
if (product == null)
{
errors.Add($"Product with ID {item.ProductId} not found");
}
else if (!product.IsActive)
{
errors.Add($"Product {product.Name} is not available");
}
else if (product.StockQuantity < item.Quantity)
{
errors.Add($"Insufficient stock for {product.Name}. Available: {product.StockQuantity}");
}
}
return new ValidationResult(errors);
}
private async Task<decimal> CalculateOrderTotalAsync(List<OrderItemRequest> items)
{
decimal total = 0;
foreach (var item in items)
{
var price = await GetProductPriceAsync(item.ProductId);
total += price * item.Quantity;
}
return total;
}
private async Task<decimal> GetProductPriceAsync(int productId)
{
var product = await _unitOfWork.Products.GetByIdAsync(productId);
return product?.Price ?? 0;
}
}
/// <summary>
/// Process payment command handler
/// </summary>
public class ProcessPaymentCommandHandler : ICommandHandler<ProcessPaymentCommand, PaymentResult>
{
private readonly IUnitOfWork _unitOfWork;
private readonly IPaymentFactory _paymentFactory;
private readonly IMediator _mediator;
private readonly ILogger<ProcessPaymentCommandHandler> _logger;
public ProcessPaymentCommandHandler(
IUnitOfWork unitOfWork,
IPaymentFactory paymentFactory,
IMediator mediator,
ILogger<ProcessPaymentCommandHandler> logger)
{
_unitOfWork = unitOfWork;
_paymentFactory = paymentFactory;
_mediator = mediator;
_logger = logger;
}
public async Task<PaymentResult> Handle(ProcessPaymentCommand command, CancellationToken cancellationToken)
{
_logger.LogInformation("Processing payment for order {OrderId}", command.OrderId);
var order = await _unitOfWork.Orders.GetByIdAsync(command.OrderId);
if (order == null)
{
return new PaymentResult
{
Success = false,
Message = $"Order {command.OrderId} not found"
};
}
// Create payment method
var paymentMethod = _paymentFactory.CreatePaymentMethod(
command.PaymentMethod, command.PaymentInfo);
if (!paymentMethod.Validate())
{
return new PaymentResult
{
Success = false,
Message = "Invalid payment information"
};
}
// Process payment
var result = await paymentMethod.ProcessPaymentAsync(order.TotalAmount);
// Update order status based on payment result
if (result.Success)
{
order.Status = OrderStatus.Confirmed;
await _unitOfWork.Orders.UpdateAsync(order);
// Publish payment processed notification
await _mediator.Publish(new PaymentProcessedNotification(order.Id, true, result.TransactionId));
}
_logger.LogInformation("Payment processing for order {OrderId} completed: {Success}",
command.OrderId, result.Success);
return result;
}
}
/// <summary>
/// Get order details query handler
/// </summary>
public class GetOrderDetailsQueryHandler : IQueryHandler<GetOrderDetailsQuery, OrderDetailsResult>
{
private readonly IOrderRepository _orderRepository;
private readonly ILogger<GetOrderDetailsQueryHandler> _logger;
public GetOrderDetailsQueryHandler(
IOrderRepository orderRepository,
ILogger<GetOrderDetailsQueryHandler> logger)
{
_orderRepository = orderRepository;
_logger = logger;
}
public async Task<OrderDetailsResult> Handle(GetOrderDetailsQuery query, CancellationToken cancellationToken)
{
_logger.LogDebug("Retrieving details for order {OrderId}", query.OrderId);
var order = await _orderRepository.GetOrderWithItemsAsync(query.OrderId);
if (order == null)
{
_logger.LogDebug("Order {OrderId} not found", query.OrderId);
return OrderDetailsResult.NotFound(query.OrderId);
}
return OrderDetailsResult.Success(order);
}
}
/// <summary>
/// Notification handlers
/// </summary>
public class OrderCreatedNotificationHandler : INotificationHandler<OrderCreatedNotification>
{
private readonly INotificationService _notificationService;
private readonly IUserRepository _userRepository;
private readonly ILogger<OrderCreatedNotificationHandler> _logger;
public OrderCreatedNotificationHandler(
INotificationService notificationService,
IUserRepository userRepository,
ILogger<OrderCreatedNotificationHandler> logger)
{
_notificationService = notificationService;
_userRepository = userRepository;
_logger = logger;
}
public async Task Handle(OrderCreatedNotification notification, CancellationToken cancellationToken)
{
_logger.LogInformation("Handling order created notification for order {OrderId}", notification.OrderId);
var user = await _userRepository.GetByIdAsync(notification.UserId);
if (user == null) return;
var message = $"Your order #{notification.OrderId} has been created. Total: {notification.TotalAmount:C}";
var subject = $"Order Created - #{notification.OrderId}";
await _notificationService.SendAsync(message, user.Email, subject);
}
}
public class PaymentProcessedNotificationHandler : INotificationHandler<PaymentProcessedNotification>
{
private readonly ILogger<PaymentProcessedNotificationHandler> _logger;
public PaymentProcessedNotificationHandler(ILogger<PaymentProcessedNotificationHandler> logger)
{
_logger = logger;
}
public async Task Handle(PaymentProcessedNotification notification, CancellationToken cancellationToken)
{
_logger.LogInformation(
"Payment for order {OrderId} processed: Success={Success}, Transaction={TransactionId}",
notification.OrderId, notification.Success, notification.TransactionId);
// Additional payment processing logic can be added here
await Task.CompletedTask;
}
}}
Step 5: Mediator Implementation
// Patterns/Mediators/Mediator.csnamespace ECommercePatterns.Patterns.Mediators{
/// <summary>
/// Mediator implementation using dependency injection
/// </summary>
public class Mediator : IMediator
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<Mediator> _logger;
public Mediator(IServiceProvider serviceProvider, ILogger<Mediator> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public async Task<TResponse> Send<TResponse>(ICommand<TResponse> command)
{
_logger.LogDebug("Sending command: {CommandType}", command.GetType().Name);
var handlerType = typeof(ICommandHandler<,>).MakeGenericType(command.GetType(), typeof(TResponse));
var handler = _serviceProvider.GetService(handlerType);
if (handler == null)
{
throw new InvalidOperationException($"No handler registered for command {command.GetType().Name}");
}
var method = handlerType.GetMethod("Handle");
var result = await (Task<TResponse>)method.Invoke(handler, new object[] { command, CancellationToken.None });
_logger.LogDebug("Command {CommandType} handled successfully", command.GetType().Name);
return result;
}
public async Task<TResponse> Send<TResponse>(IQuery<TResponse> query)
{
_logger.LogDebug("Sending query: {QueryType}", query.GetType().Name);
var handlerType = typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResponse));
var handler = _serviceProvider.GetService(handlerType);
if (handler == null)
{
throw new InvalidOperationException($"No handler registered for query {query.GetType().Name}");
}
var method = handlerType.GetMethod("Handle");
var result = await (Task<TResponse>)method.Invoke(handler, new object[] { query, CancellationToken.None });
_logger.LogDebug("Query {QueryType} handled successfully", query.GetType().Name);
return result;
}
public async Task Publish<TNotification>(TNotification notification) where TNotification : INotification
{
_logger.LogDebug("Publishing notification: {NotificationType}", notification.GetType().Name);
var handlerType = typeof(INotificationHandler<>).MakeGenericType(notification.GetType());
var handlers = _serviceProvider.GetServices(handlerType);
var tasks = handlers.Select(handler =>
{
var method = handlerType.GetMethod("Handle");
return (Task)method.Invoke(handler, new object[] { notification, CancellationToken.None });
});
await Task.WhenAll(tasks);
_logger.LogDebug("Notification {NotificationType} published to {HandlerCount} handlers",
notification.GetType().Name, handlers.Count());
}
}}
Step 6: Controller Using Mediator
// Controllers/OrdersMediatorController.csusing ECommercePatterns.Patterns.Mediators;
namespace ECommercePatterns.Controllers{
/// <summary>
/// Orders controller using Mediator pattern
/// </summary>
[ApiController]
[Route("api/mediator/[controller]")]
public class OrdersMediatorController : ControllerBase
{
private readonly IMediator _mediator;
private readonly ILogger<OrdersMediatorController> _logger;
public OrdersMediatorController(IMediator mediator, ILogger<OrdersMediatorController> logger)
{
_mediator = mediator;
_logger = logger;
}
[HttpPost]
public async Task<ActionResult<OrderResult>> CreateOrder([FromBody] CreateOrderRequest request)
{
_logger.LogInformation("Creating order via mediator for user {UserId}", request.UserId);
var command = new CreateOrderCommand(request.UserId, request.Items, request.ShippingAddress);
var result = await _mediator.Send(command);
if (result.Success)
{
return CreatedAtAction(nameof(GetOrder), new { id = result.OrderId }, result);
}
return BadRequest(result);
}
[HttpPost("{id}/payments")]
public async Task<ActionResult<PaymentResult>> ProcessPayment(int id, [FromBody] ProcessPaymentRequest request)
{
_logger.LogInformation("Processing payment via mediator for order {OrderId}", id);
var command = new ProcessPaymentCommand(id, request.PaymentMethod, request.PaymentInfo);
var result = await _mediator.Send(command);
if (result.Success)
{
return Ok(result);
}
return BadRequest(result);
}
[HttpGet("{id}")]
public async Task<ActionResult<OrderDetailsResult>> GetOrder(int id)
{
_logger.LogDebug("Retrieving order details via mediator for order {OrderId}", id);
var query = new GetOrderDetailsQuery(id);
var result = await _mediator.Send(query);
if (result.Found)
{
return Ok(result);
}
return NotFound(result);
}
[HttpGet("user/{userId}")]
public async Task<ActionResult<UserOrdersResult>> GetUserOrders(int userId, [FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
_logger.LogDebug("Retrieving user orders via mediator for user {UserId}", userId);
var query = new GetUserOrdersQuery(userId, page, pageSize);
var result = await _mediator.Send(query);
return Ok(result);
}
}}
10. Pattern Combinations in Real Applications
E-Commerce Service Combining Multiple Patterns
// Services/ECommerceOrchestrationService.csnamespace ECommercePatterns.Services{
/// <summary>
/// E-commerce orchestration service combining multiple patterns
/// </summary>
public interface IECommerceOrchestrationService
{
Task<OrderResult> PlaceOrderAsync(PlaceOrderRequest request);
Task<OrderFulfillmentResult> FulfillOrderAsync(int orderId);
Task<OrderAnalysisResult> AnalyzeOrderPatternsAsync(DateTime startDate, DateTime endDate);
}
public class ECommerceOrchestrationService : IECommerceOrchestrationService
{
private readonly IMediator _mediator;
private readonly IEventBus _eventBus;
private readonly IShippingCalculator _shippingCalculator;
private readonly INotificationServiceFactory _notificationFactory;
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<ECommerceOrchestrationService> _logger;
public ECommerceOrchestrationService(
IMediator mediator,
IEventBus eventBus,
IShippingCalculator shippingCalculator,
INotificationServiceFactory notificationFactory,
IUnitOfWork unitOfWork,
ILogger<ECommerceOrchestrationService> logger)
{
_mediator = mediator;
_eventBus = eventBus;
_shippingCalculator = shippingCalculator;
_notificationFactory = notificationFactory;
_unitOfWork = unitOfWork;
_logger = logger;
}
public async Task<OrderResult> PlaceOrderAsync(PlaceOrderRequest request)
{
_logger.LogInformation("Orchestrating order placement for user {UserId}", request.UserId);
// Use Mediator pattern for command handling
var command = new CreateOrderCommand(request.UserId, request.Items, request.ShippingAddress);
var result = await _mediator.Send(command);
if (result.Success)
{
// Use Observer pattern for event publishing
await _eventBus.PublishAsync(new OrderCreatedEvent(
result.OrderId, request.UserId, result.TotalAmount, DateTime.UtcNow));
// Use Strategy pattern for shipping calculation
var shippingContext = new ShippingContext
{
Weight = CalculateTotalWeight(request.Items),
Distance = CalculateDistance(request.ShippingAddress),
Destination = request.ShippingAddress.City
};
var shippingOptions = _shippingCalculator.GetAvailableOptions(shippingContext);
// Use Decorator pattern for notifications
var notificationService = _notificationFactory.CreateServiceWithAllDecorators();
await notificationService.SendAsync(
"Order placed successfully",
await GetUserEmailAsync(request.UserId),
"Order Confirmation");
}
return result;
}
public async Task<OrderFulfillmentResult> FulfillOrderAsync(int orderId)
{
_logger.LogInformation("Orchestrating order fulfillment for order {OrderId}", orderId);
// Repository pattern for data access
var order = await _unitOfWork.Orders.GetOrderWithItemsAsync(orderId);
if (order == null)
{
return OrderFulfillmentResult.Failure($"Order {orderId} not found");
}
// Factory pattern for payment processing
var paymentCommand = new ProcessPaymentCommand(orderId, "CreditCard", new PaymentInfo());
var paymentResult = await _mediator.Send(paymentCommand);
if (!paymentResult.Success)
{
return OrderFulfillmentResult.Failure($"Payment failed: {paymentResult.Message}");
}
// Unit of Work pattern for transaction management
await _unitOfWork.BeginTransactionAsync();
try
{
// Update order status
order.Status = OrderStatus.Processing;
await _unitOfWork.Orders.UpdateAsync(order);
// Singleton pattern for global statistics
var statsService = GlobalStatisticsService.Instance;
await statsService.RecordOrderAsync(order.TotalAmount);
await _unitOfWork.CommitTransactionAsync();
// Observer pattern for event publishing
await _eventBus.PublishAsync(new PaymentReceivedEvent(
orderId, order.TotalAmount, "CreditCard", paymentResult.TransactionId));
return OrderFulfillmentResult.Success(orderId);
}
catch (Exception ex)
{
await _unitOfWork.RollbackTransactionAsync();
_logger.LogError(ex, "Error fulfilling order {OrderId}", orderId);
return OrderFulfillmentResult.Failure("Order fulfillment failed");
}
}
public async Task<OrderAnalysisResult> AnalyzeOrderPatternsAsync(DateTime startDate, DateTime endDate)
{
_logger.LogInformation("Analyzing order patterns from {StartDate} to {EndDate}", startDate, endDate);
// Repository pattern with specifications
var orders = await _unitOfWork.Orders.FindAsync(o =>
o.CreatedAt >= startDate && o.CreatedAt <= endDate);
// Strategy pattern for different analysis algorithms
var analysisStrategies = new List<IOrderAnalysisStrategy>
{
new RevenueAnalysisStrategy(),
new CustomerBehaviorAnalysisStrategy(),
new ProductPerformanceAnalysisStrategy()
};
var results = new List<AnalysisResult>();
foreach (var strategy in analysisStrategies)
{
var result = await strategy.AnalyzeAsync(orders.ToList());
results.Add(result);
}
return new OrderAnalysisResult
{
Success = true,
Results = results,
AnalysisPeriod = $"{startDate:yyyy-MM-dd} to {endDate:yyyy-MM-dd}"
};
}
private decimal CalculateTotalWeight(List<OrderItemRequest> items)
{
// Simplified calculation
return items.Sum(i => i.Quantity * 0.5m);
}
private decimal CalculateDistance(Address address)
{
// Simplified calculation
return address.City.ToLower() switch
{
"new york" => 400,
"los angeles" => 3800,
_ => 1000
};
}
private async Task<string> GetUserEmailAsync(int userId)
{
var user = await _unitOfWork.Users.GetByIdAsync(userId);
return user?.Email ?? string.Empty;
}
}
/// <summary>
/// Order fulfillment result
/// </summary>
public class OrderFulfillmentResult
{
public bool Success { get; }
public int OrderId { get; }
public string Message { get; }
private OrderFulfillmentResult(bool success, int orderId, string message)
{
Success = success;
OrderId = orderId;
Message = message;
}
public static OrderFulfillmentResult Success(int orderId)
=> new OrderFulfillmentResult(true, orderId, "Order fulfilled successfully");
public static OrderFulfillmentResult Failure(string message)
=> new OrderFulfillmentResult(false, 0, message);
}
/// <summary>
/// Order analysis result
/// </summary>
public class OrderAnalysisResult
{
public bool Success { get; set; }
public List<AnalysisResult> Results { get; set; } = new();
public string AnalysisPeriod { get; set; } = string.Empty;
}
/// <summary>
/// Analysis strategy interface
/// </summary>
public interface IOrderAnalysisStrategy
{
Task<AnalysisResult> AnalyzeAsync(List<Order> orders);
}
/// <summary>
/// Analysis result
/// </summary>
public class AnalysisResult
{
public string AnalysisType { get; set; } = string.Empty;
public object Data { get; set; } = new();
public string Summary { get; set; } = string.Empty;
}}
11. Testing Patterns with xUnit
Repository Pattern Testing
// Tests/Repositories/ProductRepositoryTests.csusing Microsoft.EntityFrameworkCore;
namespace ECommercePatterns.Tests.Repositories{
public class ProductRepositoryTests : IDisposable
{
private readonly ECommerceContext _context;
private readonly ProductRepository _repository;
public ProductRepositoryTests()
{
// Use in-memory database for testing
var options = new DbContextOptionsBuilder<ECommerceContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
_context = new ECommerceContext(options);
_repository = new ProductRepository(_context);
// Seed test data
SeedTestData();
}
[Fact]
public async Task GetByIdAsync_ExistingProduct_ReturnsProduct()
{
// Act
var result = await _repository.GetByIdAsync(1);
// Assert
Assert.NotNull(result);
Assert.Equal(1, result.Id);
Assert.Equal("Test Product", result.Name);
}
[Fact]
public async Task GetByIdAsync_NonExistingProduct_ReturnsNull()
{
// Act
var result = await _repository.GetByIdAsync(999);
// Assert
Assert.Null(result);
}
[Fact]
public async Task GetActiveProductsAsync_ReturnsOnlyActiveProducts()
{
// Act
var result = await _repository.GetActiveProductsAsync();
// Assert
Assert.All(result, p => Assert.True(p.IsActive));
}
[Fact]
public async Task SearchProductsAsync_FoundProducts_ReturnsMatchingProducts()
{
// Act
var result = await _repository.SearchProductsAsync("Test");
// Assert
Assert.NotEmpty(result);
Assert.All(result, p =>
Assert.True(p.Name.Contains("Test") || p.Description.Contains("Test")));
}
[Fact]
public async Task UpdateStockAsync_ValidUpdate_UpdatesStock()
{
// Arrange
var initialStock = (await _repository.GetByIdAsync(1))!.StockQuantity;
// Act
await _repository.UpdateStockAsync(1, -5);
// Assert
var updatedProduct = await _repository.GetByIdAsync(1);
Assert.Equal(initialStock - 5, updatedProduct!.StockQuantity);
}
private void SeedTestData()
{
var categories = new[]
{
new Category { Id = 1, Name = "Electronics", Description = "Electronic items" },
new Category { Id = 2, Name = "Books", Description = "Books and media" }
};
var products = new[]
{
new Product
{
Id = 1,
Name = "Test Product 1",
Description = "Test Description 1",
Price = 99.99m,
StockQuantity = 50,
IsActive = true,
CategoryId = 1
},
new Product
{
Id = 2,
Name = "Test Product 2",
Description = "Test Description 2",
Price = 49.99m,
StockQuantity = 25,
IsActive = false, // Inactive
CategoryId = 1
},
new Product
{
Id = 3,
Name = "Another Product",
Description = "Another Description",
Price = 29.99m,
StockQuantity = 10,
IsActive = true,
CategoryId = 2
}
};
_context.Categories.AddRange(categories);
_context.Products.AddRange(products);
_context.SaveChanges();
}
public void Dispose()
{
_context?.Dispose();
}
}}
Mediator Pattern Testing
// Tests/Mediators/CreateOrderCommandHandlerTests.csnamespace ECommercePatterns.Tests.Mediators{
public class CreateOrderCommandHandlerTests
{
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
private readonly Mock<IMediator> _mediatorMock;
private readonly Mock<ILogger<CreateOrderCommandHandler>> _loggerMock;
private readonly CreateOrderCommandHandler _handler;
public CreateOrderCommandHandlerTests()
{
_unitOfWorkMock = new Mock<IUnitOfWork>();
_mediatorMock = new Mock<IMediator>();
_loggerMock = new Mock<ILogger<CreateOrderCommandHandler>>();
_handler = new CreateOrderCommandHandler(
_unitOfWorkMock.Object,
_mediatorMock.Object,
_loggerMock.Object);
}
[Fact]
public async Task Handle_ValidCommand_ReturnsSuccessResult()
{
// Arrange
var command = new CreateOrderCommand(
1,
new List<OrderItemRequest>
{
new() { ProductId = 1, Quantity = 2 }
},
new Address());
var product = new Product
{
Id = 1,
Name = "Test Product",
Price = 10.0m,
StockQuantity = 10,
IsActive = true
};
var productRepoMock = new Mock<IProductRepository>();
productRepoMock.Setup(r => r.GetByIdAsync(1))
.ReturnsAsync(product);
productRepoMock.Setup(r => r.UpdateStockAsync(It.IsAny<int>(), It.IsAny<int>()))
.Returns(Task.CompletedTask);
var orderRepoMock = new Mock<IOrderRepository>();
orderRepoMock.Setup(r => r.AddAsync(It.IsAny<Order>()))
.ReturnsAsync((Order o) => o);
_unitOfWorkMock.Setup(u => u.Products).Returns(productRepoMock.Object);
_unitOfWorkMock.Setup(u => u.Orders).Returns(orderRepoMock.Object);
_unitOfWorkMock.Setup(u => u.BeginTransactionAsync()).Returns(Task.CompletedTask);
_unitOfWorkMock.Setup(u => u.CommitTransactionAsync()).Returns(Task.CompletedTask);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
Assert.True(result.Success);
Assert.True(result.OrderId > 0);
_mediatorMock.Verify(m => m.Publish(It.IsAny<OrderCreatedNotification>(), It.IsAny<CancellationToken>()),
Times.Once);
}
[Fact]
public async Task Handle_InvalidProduct_ReturnsFailureResult()
{
// Arrange
var command = new CreateOrderCommand(
1,
new List<OrderItemRequest>
{
new() { ProductId = 999, Quantity = 2 } // Non-existent product
},
new Address());
var productRepoMock = new Mock<IProductRepository>();
productRepoMock.Setup(r => r.GetByIdAsync(999))
.ReturnsAsync((Product)null);
_unitOfWorkMock.Setup(u => u.Products).Returns(productRepoMock.Object);
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
Assert.False(result.Success);
Assert.Contains("not found", result.Message);
}
}}
Strategy Pattern Testing
// Tests/Strategies/ShippingCalculatorTests.csnamespace ECommercePatterns.Tests.Strategies{
public class ShippingCalculatorTests
{
private readonly ShippingCalculator _calculator;
public ShippingCalculatorTests()
{
var loggerMock = new Mock<ILogger<ShippingCalculator>>();
_calculator = new ShippingCalculator(loggerMock.Object);
}
[Theory]
[InlineData("standard", 5.0, 100.0, "New York", 10.0)] // 5*0.5 + 100*0.1 = 2.5 + 10 = 12.5
[InlineData("express", 2.0, 50.0, "Los Angeles", 17.4)] // 2*1.2 + 50*0.3 + 10 = 2.4 + 15 + 10 = 27.4
public void CalculateShipping_ValidInputs_ReturnsExpectedCost(
string method, decimal weight, decimal distance, string destination, decimal expectedCost)
{
// Arrange
var context = new ShippingContext
{
Weight = weight,
Distance = distance,
Destination = destination
};
// Act
var result = _calculator.CalculateShipping(context, method);
// Assert
Assert.True(result.IsAvailable);
Assert.Equal(expectedCost, result.Cost, 2);
}
[Fact]
public void GetAvailableOptions_ValidContext_ReturnsAvailableOptions()
{
// Arrange
var context = new ShippingContext
{
Weight = 2.0m,
Distance = 100.0m,
Destination = "New York"
};
// Act
var options = _calculator.GetAvailableOptions(context);
// Assert
Assert.NotEmpty(options);
Assert.All(options, o => Assert.True(o.IsAvailable));
}
[Fact]
public void CalculateShipping_RestrictedDestination_ReturnsNotAvailable()
{
// Arrange
var context = new ShippingContext
{
Weight = 2.0m,
Distance = 100.0m,
Destination = "Cuba" // Restricted destination
};
// Act
var result = _calculator.CalculateShipping(context, "standard");
// Assert
Assert.False(result.IsAvailable);
}
}}
12. Performance Considerations
Repository Pattern Performance
// Services/PerformanceOptimizedRepository.csnamespace ECommercePatterns.Services{
/// <summary>
/// Performance-optimized repository with caching
/// </summary>
public class CachedProductRepository : IProductRepository
{
private readonly IProductRepository _decorated;
private readonly ICacheService _cacheService;
private readonly ILogger<CachedProductRepository> _logger;
private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(30);
public CachedProductRepository(
IProductRepository decorated,
ICacheService cacheService,
ILogger<CachedProductRepository> logger)
{
_decorated = decorated;
_cacheService = cacheService;
_logger = logger;
}
public async Task<Product?> GetByIdAsync(int id)
{
var cacheKey = $"product_{id}";
var cachedProduct = await _cacheService.GetAsync<Product>(cacheKey);
if (cachedProduct != null)
{
_logger.LogDebug("Cache hit for product {ProductId}", id);
return cachedProduct;
}
_logger.LogDebug("Cache miss for product {ProductId}", id);
var product = await _decorated.GetByIdAsync(id);
if (product != null)
{
await _cacheService.SetAsync(cacheKey, product, _cacheDuration);
}
return product;
}
public async Task UpdateAsync(Product entity)
{
await _decorated.UpdateAsync(entity);
// Invalidate cache
var cacheKey = $"product_{entity.Id}";
await _cacheService.RemoveAsync(cacheKey);
_logger.LogDebug("Invalidated cache for product {ProductId}", entity.Id);
}
// Implement other interface members by delegating to _decorated
public Task<IEnumerable<Product>> GetAllAsync() => _decorated.GetAllAsync();
public Task<IEnumerable<Product>> FindAsync(Expression<Func<Product, bool>> predicate)
=> _decorated.FindAsync(predicate);
public Task<Product> AddAsync(Product entity) => _decorated.AddAsync(entity);
public Task DeleteAsync(Product entity) => _decorated.DeleteAsync(entity);
public Task<bool> ExistsAsync(int id) => _decorated.ExistsAsync(id);
public Task<int> CountAsync(Expression<Func<Product, bool>>? predicate = null)
=> _decorated.CountAsync(predicate);
// Implement other IProductRepository methods...
}}
Lazy Loading with Proxy Pattern
// Patterns/Proxies/LazyProductRepository.csnamespace ECommercePatterns.Patterns.Proxies{
/// <summary>
/// Proxy for lazy loading of product details
/// </summary>
public class LazyProductRepository : IProductRepository
{
private readonly Lazy<IProductRepository> _lazyRepository;
private readonly IServiceProvider _serviceProvider;
public LazyProductRepository(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
_lazyRepository = new Lazy<IProductRepository>(() =>
_serviceProvider.GetRequiredService<ProductRepository>());
}
private IProductRepository Repository => _lazyRepository.Value;
public Task<Product?> GetByIdAsync(int id) => Repository.GetByIdAsync(id);
public Task<IEnumerable<Product>> GetAllAsync() => Repository.GetAllAsync();
public Task<IEnumerable<Product>> FindAsync(Expression<Func<Product, bool>> predicate)
=> Repository.FindAsync(predicate);
public Task<Product> AddAsync(Product entity) => Repository.AddAsync(entity);
public Task UpdateAsync(Product entity) => Repository.UpdateAsync(entity);
public Task DeleteAsync(Product entity) => Repository.DeleteAsync(entity);
public Task<bool> ExistsAsync(int id) => Repository.ExistsAsync(id);
public Task<int> CountAsync(Expression<Func<Product, bool>>? predicate = null)
=> Repository.CountAsync(predicate);
// Implement other IProductRepository methods...
}}
13. Anti-Patterns to Avoid
1. God Service Anti-Pattern
// β Anti-Pattern: God Service doing everythingpublic class GodService{
private readonly IUserRepository _userRepository;
private readonly IProductRepository _productRepository;
private readonly IOrderRepository _orderRepository;
private readonly IEmailService _emailService;
private readonly IPaymentService _paymentService;
private readonly IShippingService _shippingService;
private readonly IAnalyticsService _analyticsService;
// ... 10 more dependencies
public async Task ProcessOrder(Order order)
{
// User management
// Product management
// Order processing
// Payment handling
// Shipping calculation
// Email sending
// Analytics recording
// Logging
// Caching
// Validation
// 1000+ lines of code...
}}
// β
Solution: Separate concerns using patternspublic class OrderProcessingService // Focused responsibilitypublic class PaymentProcessingService // Single responsibility public class ShippingCalculationService // Specific domainpublic class NotificationService // Clear purpose
2. Circular Dependency Anti-Pattern
// β Anti-Pattern: Circular dependenciespublic class ServiceA{
private readonly ServiceB _serviceB;
public ServiceA(ServiceB serviceB) => _serviceB = serviceB;
public void MethodA() => _serviceB.MethodB();}
public class ServiceB
{
private readonly ServiceA _serviceA;
public ServiceB(ServiceA serviceA) => _serviceA = serviceA;
public void MethodB() => _serviceA.MethodA(); // Circular dependency!}
// β
Solution: Use Mediator or Event patternspublic class ServiceA{
private readonly IMediator _mediator;
public ServiceA(IMediator mediator) => _mediator = mediator;
public async Task MethodA() => await _mediator.Publish(new EventA());}
public class ServiceB{
private readonly IMediator _mediator;
public ServiceB(IMediator mediator) => _mediator = mediator;
public async Task HandleEventA(EventA @event) { /* Handle event */ }}
3. Premature Optimization Anti-Pattern
// β Anti-Pattern: Complex optimization before measuringpublic class OverOptimizedService{
// Complex caching, parallel processing, micro-optimizations
// that make code hard to read and maintain
// without proven performance benefits}
// β
Solution: Measure first, optimize based on datapublic class MeasuredOptimizationService{
// Start with clean, maintainable code
// Profile to identify bottlenecks
// Apply targeted optimizations only where needed}
14. Conclusion
The Power of Design Patterns in ASP.NET Core
Design patterns are not just academic conceptsβthey're practical tools that solve real-world problems in software development. Throughout this comprehensive guide, we've explored how these patterns can transform your ASP.NET Core applications:
Repository Pattern : Clean data access abstraction that makes your code testable and maintainable
Unit of Work : Transaction management that ensures data consistency
Singleton : Shared instance management for resources that should have single instances
Factory : Flexible object creation that follows Open/Closed principle
Strategy : Interchangeable algorithms that make your code extensible
Decorator : Dynamic behavior extension without modifying existing code
Observer : Loose-coupled event-driven architecture
Mediator : Decoupled communication between components
Real-World Impact
By applying these patterns, you'll create applications that are:
β
Testable : Easy to write unit tests with mocked dependencies
β
Maintainable : Clear separation of concerns and responsibilities
β
Scalable : Architecture that grows with your business needs
β
Flexible : Easy to extend and modify without breaking existing code
β
Reliable : Robust error handling and consistent behavior
Continuing Your Journey
Design patterns are tools in your toolbox, not rigid rules. The true mastery comes from understanding when and how to apply them appropriately. Remember:
Start Simple : Don't over-engineer. Use patterns when they provide clear benefits
Combine Wisely : Patterns work best when combined thoughtfully
Measure Performance : Always validate that your patterns improve rather than hinder performance
Keep Learning : Patterns evolve with new technologies and practices
Remember : The goal isn't to use every pattern everywhere, but to have the right pattern for each problem you encounter in your development journey.