ASP.NET Core  

Master Repository & Unit of Work Patterns in ASP.NET Core (Part-37 of 40)

Master-Repositor

Previous article: CQRS and MediatR Masterclass: Solve Business Complexity in ASP.NET Core (Part-36 of 40)

📋 Table of Contents

  1. Introduction to Data Access Patterns

  2. Why We Need These Patterns

  3. Repository Pattern Deep Dive

  4. Unit of Work Pattern Explained

  5. Real-World E-Commerce Implementation

  6. Advanced Implementation Scenarios

  7. Testing Strategies

  8. Performance Optimization

  9. Common Pitfalls & Best Practices

  10. Alternatives & When to Use What

1. Introduction to Data Access Patterns

The Problem with Direct Data Access

Imagine you're building an e-commerce application. Without proper patterns, your controllers might look like this:

  
    // ❌ DON'T: Direct data access in controllerspublic class ProductController : Controller{
    private readonly ApplicationDbContext _context;

    public ProductController(ApplicationDbContext context)
    {
        _context = context;
    }

    public IActionResult GetProducts()
    {
        var products = _context.Products
            .Include(p => p.Category)
            .Include(p => p.Inventory)
            .Where(p => p.IsActive)
            .ToList();
        
        return View(products);
    }

    public IActionResult CreateProduct(Product product)
    {
        _context.Products.Add(product);
        _context.SaveChanges(); // What if this fails?
        return RedirectToAction("GetProducts");
    }}
  

Problems with this approach:

  • Tight coupling  between controllers and Entity Framework

  • Difficult to test  - requires mocking DbContext

  • Code duplication  across multiple controllers

  • No abstraction  - changing data access technology requires massive refactoring

  • Transaction management  is manual and error-prone

The Solution: Repository & Unit of Work Patterns

  
    // ✅ DO: Clean, testable controllerspublic class ProductController : Controller{
    private readonly IUnitOfWork _unitOfWork;

    public ProductController(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public async Task<IActionResult> GetProducts()
    {
        var products = await _unitOfWork.Products
            .GetActiveProductsWithDetailsAsync();
        
        return View(products);
    }

    public async Task<IActionResult> CreateProduct(Product product)
    {
        _unitOfWork.Products.Add(product);
        await _unitOfWork.CommitAsync(); // Atomic operation
        return RedirectToAction("GetProducts");
    }}
  

2. Why We Need These Patterns

Real-World Scenario: E-Commerce Platform

Consider an e-commerce order processing system:

  
    // ❌ Problematic approach without patternspublic async Task ProcessOrder(Order order){
    // This method has too many responsibilities
    using var transaction = await _context.Database.BeginTransactionAsync();
    
    try
    {
        // 1. Update inventory
        foreach (var item in order.Items)
        {
            var product = await _context.Products.FindAsync(item.ProductId);
            product.StockQuantity -= item.Quantity;
            
            if (product.StockQuantity < 0)
                throw new Exception("Insufficient stock");
        }
        
        // 2. Create order
        _context.Orders.Add(order);
        
        // 3. Process payment
        var payment = new Payment 
        { 
            OrderId = order.Id, 
            Amount = order.TotalAmount 
        };
        _context.Payments.Add(payment);
        
        // 4. Send notification
        var customer = await _context.Customers.FindAsync(order.CustomerId);
        await _emailService.SendOrderConfirmation(customer.Email, order);
        
        await _context.SaveChangesAsync();
        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }}
  

Benefits of Using Patterns

  1. Testability: Mock repositories instead of complex DbContext

  2. Maintainability: Centralize data access logic

  3. Flexibility: Switch data providers easily

  4. Consistency: Enforce business rules uniformly

  5. Performance: Implement caching and optimization centrally

3. Repository Pattern Deep Dive

Core Repository Interface

  
    // Core/Interfaces/IRepository.cspublic interface IRepository<T> where T : BaseEntity{
    Task<T> GetByIdAsync(int id);
    Task<IReadOnlyList<T>> GetAllAsync();
    Task<T> AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(T entity);
    Task<bool> ExistsAsync(int id);
    Task<int> CountAsync();
    Task<IReadOnlyList<T>> GetPagedAsync(int pageNumber, int pageSize);}
  

Generic Repository Implementation

  
    // Infrastructure/Data/Repository.cspublic class Repository<T> : IRepository<T> where T : BaseEntity{
    protected readonly ApplicationDbContext _context;
    protected readonly DbSet<T> _dbSet;

    public Repository(ApplicationDbContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
    }

    public virtual async Task<T> GetByIdAsync(int id)
    {
        return await _dbSet.FindAsync(id);
    }

    public virtual async Task<IReadOnlyList<T>> GetAllAsync()
    {
        return await _dbSet.ToListAsync();
    }

    public virtual async Task<T> AddAsync(T entity)
    {
        await _dbSet.AddAsync(entity);
        return entity;
    }

    public virtual async Task UpdateAsync(T entity)
    {
        _context.Entry(entity).State = EntityState.Modified;
        await Task.CompletedTask;
    }

    public virtual async Task DeleteAsync(T entity)
    {
        _dbSet.Remove(entity);
        await Task.CompletedTask;
    }

    public virtual async Task<bool> ExistsAsync(int id)
    {
        return await _dbSet.AnyAsync(e => e.Id == id);
    }

    public virtual async Task<int> CountAsync()
    {
        return await _dbSet.CountAsync();
    }

    public virtual async Task<IReadOnlyList<T>> GetPagedAsync(int pageNumber, int pageSize)
    {
        return await _dbSet
            .Skip((pageNumber - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();
    }}
  

Specialized Repository Interfaces

  
    // Core/Interfaces/IProductRepository.cspublic interface IProductRepository : IRepository<Product>{
    Task<Product> GetProductWithCategoryAsync(int productId);
    Task<IReadOnlyList<Product>> GetProductsByCategoryAsync(int categoryId);
    Task<IReadOnlyList<Product>> SearchProductsAsync(string searchTerm, int pageNumber, int pageSize);
    Task<IReadOnlyList<Product>> GetActiveProductsWithDetailsAsync();
    Task UpdateProductStockAsync(int productId, int quantityChange);
    Task<bool> IsProductNameUnique(string productName, int? excludeProductId = null);}

// Core/Interfaces/IOrderRepository.cspublic interface IOrderRepository : IRepository<Order>{
    Task<Order> GetOrderWithDetailsAsync(int orderId);
    Task<IReadOnlyList<Order>> GetOrdersByCustomerAsync(int customerId);
    Task<IReadOnlyList<Order>> GetPendingOrdersAsync();
    Task<Order> CreateOrderFromCartAsync(int cartId, string customerId);
    Task UpdateOrderStatusAsync(int orderId, OrderStatus status);}
  

Specialized Repository Implementations

  
    // Infrastructure/Data/Repositories/ProductRepository.cspublic class ProductRepository : Repository<Product>, IProductRepository{
    public ProductRepository(ApplicationDbContext context) : base(context)
    {
    }

    public async Task<Product> GetProductWithCategoryAsync(int productId)
    {
        return await _context.Products
            .Include(p => p.Category)
            .Include(p => p.Inventory)
            .FirstOrDefaultAsync(p => p.Id == productId);
    }

    public async Task<IReadOnlyList<Product>> GetProductsByCategoryAsync(int categoryId)
    {
        return await _context.Products
            .Where(p => p.CategoryId == categoryId && p.IsActive)
            .Include(p => p.Category)
            .OrderBy(p => p.Name)
            .ToListAsync();
    }

    public async Task<IReadOnlyList<Product>> SearchProductsAsync(string searchTerm, int pageNumber, int pageSize)
    {
        return await _context.Products
            .Where(p => p.IsActive && 
                       (p.Name.Contains(searchTerm) || 
                        p.Description.Contains(searchTerm) ||
                        p.Category.Name.Contains(searchTerm)))
            .Include(p => p.Category)
            .OrderBy(p => p.Name)
            .Skip((pageNumber - 1) * pageSize)
            .Take(pageSize)
            .ToListAsync();
    }

    public async Task<IReadOnlyList<Product>> GetActiveProductsWithDetailsAsync()
    {
        return await _context.Products
            .Where(p => p.IsActive)
            .Include(p => p.Category)
            .Include(p => p.Inventory)
            .Include(p => p.Reviews)
            .OrderBy(p => p.Category.Name)
            .ThenBy(p => p.Name)
            .ToListAsync();
    }

    public async Task UpdateProductStockAsync(int productId, int quantityChange)
    {
        var product = await _context.Products.FindAsync(productId);
        if (product != null)
        {
            product.StockQuantity += quantityChange;
            if (product.StockQuantity < 0)
                throw new InvalidOperationException("Insufficient stock");
                
            product.LastStockUpdate = DateTime.UtcNow;
        }
    }

    public async Task<bool> IsProductNameUnique(string productName, int? excludeProductId = null)
    {
        return !await _context.Products
            .AnyAsync(p => p.Name == productName && 
                          p.Id != excludeProductId);
    }}
  

4. Unit of Work Pattern Explained

Unit of Work Interface

  
    // Core/Interfaces/IUnitOfWork.cspublic interface IUnitOfWork : IDisposable{
    // Generic repositories
    IRepository<T> Repository<T>() where T : BaseEntity;
    
    // Specific repositories
    IProductRepository Products { get; }
    IOrderRepository Orders { get; }
    ICustomerRepository Customers { get; }
    ICategoryRepository Categories { get; }
    IPaymentRepository Payments { get; }
    
    // Transaction management
    Task<int> CommitAsync();
    Task RollbackAsync();
    Task BeginTransactionAsync();
    Task CommitTransactionAsync();
    
    // Change tracking
    void DetachAllEntities();
    void ClearChangeTracker();}
  

Unit of Work Implementation

  
    // Infrastructure/Data/UnitOfWork.cspublic class UnitOfWork : IUnitOfWork{
    private readonly ApplicationDbContext _context;
    private readonly ILogger<UnitOfWork> _logger;
    private IDbContextTransaction _transaction;
    private bool _disposed = false;

    // Specific repositories
    public IProductRepository Products { get; }
    public IOrderRepository Orders { get; }
    public ICustomerRepository Customers { get; }
    public ICategoryRepository Categories { get; }
    public IPaymentRepository Payments { get; }

    // Repository cache
    private Dictionary<Type, object> _repositories;

    public UnitOfWork(ApplicationDbContext context, ILogger<UnitOfWork> logger)
    {
        _context = context;
        _logger = logger;
        
        // Initialize specific repositories
        Products = new ProductRepository(_context);
        Orders = new OrderRepository(_context);
        Customers = new CustomerRepository(_context);
        Categories = new CategoryRepository(_context);
        Payments = new PaymentRepository(_context);
        
        _repositories = new Dictionary<Type, object>();
    }

    public IRepository<T> Repository<T>() where T : BaseEntity
    {
        var type = typeof(T);
        if (!_repositories.ContainsKey(type))
        {
            _repositories[type] = new Repository<T>(_context);
        }
        return (IRepository<T>)_repositories[type];
    }

    public async Task<int> CommitAsync()
    {
        try
        {
            // Audit trail - automatically set modified dates
            var entries = _context.ChangeTracker.Entries<BaseEntity>()
                .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);

            foreach (var entry in entries)
            {
                if (entry.State == EntityState.Added)
                {
                    entry.Entity.CreatedAt = DateTime.UtcNow;
                    entry.Entity.CreatedBy = "system"; // Get from current user in real scenario
                }
                entry.Entity.UpdatedAt = DateTime.UtcNow;
                entry.Entity.UpdatedBy = "system"; // Get from current user in real scenario
            }

            return await _context.SaveChangesAsync();
        }
        catch (DbUpdateException ex)
        {
            _logger.LogError(ex, "Database update error occurred");
            throw new RepositoryException("An error occurred while saving changes to the database", ex);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unexpected error occurred during commit");
            throw;
        }
    }

    public async Task BeginTransactionAsync()
    {
        if (_transaction != null)
        {
            throw new InvalidOperationException("A transaction is already in progress");
        }
        
        _transaction = await _context.Database.BeginTransactionAsync();
        _logger.LogInformation("Database transaction started");
    }

    public async Task CommitTransactionAsync()
    {
        if (_transaction == null)
        {
            throw new InvalidOperationException("No transaction to commit");
        }

        try
        {
            await _transaction.CommitAsync();
            _logger.LogInformation("Database transaction committed");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error committing transaction");
            await _transaction.RollbackAsync();
            throw;
        }
        finally
        {
            _transaction.Dispose();
            _transaction = null;
        }
    }

    public async Task RollbackAsync()
    {
        if (_transaction != null)
        {
            await _transaction.RollbackAsync();
            _transaction.Dispose();
            _transaction = null;
            _logger.LogInformation("Database transaction rolled back");
        }
        
        // Clear change tracker to prevent stale data
        ClearChangeTracker();
    }

    public void DetachAllEntities()
    {
        var entries = _context.ChangeTracker.Entries()
            .Where(e => e.State != EntityState.Detached)
            .ToList();

        foreach (var entry in entries)
        {
            entry.State = EntityState.Detached;
        }
    }

    public void ClearChangeTracker()
    {
        _context.ChangeTracker.Clear();
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed && disposing)
        {
            _transaction?.Dispose();
            _context?.Dispose();
        }
        _disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }}
  

5. Real-World E-Commerce Implementation

Complete Domain Models

  
    // Core/Entities/BaseEntity.cspublic abstract class BaseEntity{
    public int Id { get; set; }
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
    public string CreatedBy { get; set; } = "system";
    public string UpdatedBy { get; set; } = "system";
    public bool IsActive { get; set; } = true;}

// Core/Entities/Product.cspublic class Product : BaseEntity{
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public decimal? DiscountPrice { get; set; }
    public int StockQuantity { get; set; }
    public string SKU { get; set; }
    public string ImageUrl { get; set; }
    
    // Relationships
    public int CategoryId { get; set; }
    public Category Category { get; set; }
    
    public ICollection<OrderItem> OrderItems { get; set; }
    public ICollection<Review> Reviews { get; set; }
    public Inventory Inventory { get; set; }

    // Computed properties
    public decimal CurrentPrice => DiscountPrice ?? Price;
    public bool InStock => StockQuantity > 0;
    public double AverageRating => Reviews?.Any() == true ? 
        Reviews.Average(r => r.Rating) : 0;}

// Core/Entities/Order.cspublic class Order : BaseEntity{
    public string OrderNumber { get; set; } = GenerateOrderNumber();
    public DateTime OrderDate { get; set; } = DateTime.UtcNow;
    public decimal TotalAmount { get; set; }
    public OrderStatus Status { get; set; } = OrderStatus.Pending;
    
    // Customer information
    public int CustomerId { get; set; }
    public Customer Customer { get; set; }
    
    // Shipping information
    public string ShippingAddress { get; set; }
    public string ShippingCity { get; set; }
    public string ShippingZipCode { get; set; }
    
    // Navigation properties
    public ICollection<OrderItem> OrderItems { get; set; }
    public Payment Payment { get; set; }

    private static string GenerateOrderNumber()
    {
        return $"ORD-{DateTime.UtcNow:yyyyMMdd-HHmmss}-{Guid.NewGuid().ToString()[..8].ToUpper()}";
    }}

// Core/Enums/OrderStatus.cspublic enum OrderStatus{
    Pending,
    Confirmed,
    Processing,
    Shipped,
    Delivered,
    Cancelled,
    Refunded
}
  

Advanced Service Layer

  
    // Core/Interfaces/IOrderService.cspublic interface IOrderService{
    Task<OrderResult> CreateOrderAsync(CreateOrderRequest request);
    Task<Order> ProcessOrderAsync(int orderId);
    Task CancelOrderAsync(int orderId);
    Task<Order> GetOrderWithDetailsAsync(int orderId);
    Task<IEnumerable<Order>> GetCustomerOrdersAsync(int customerId);
    Task UpdateOrderStatusAsync(int orderId, OrderStatus status);}

// Infrastructure/Services/OrderService.cspublic class OrderService : IOrderService{
    private readonly IUnitOfWork _unitOfWork;
    private readonly IEmailService _emailService;
    private readonly IPaymentService _paymentService;
    private readonly ILogger<OrderService> _logger;

    public OrderService(
        IUnitOfWork unitOfWork,
        IEmailService emailService,
        IPaymentService paymentService,
        ILogger<OrderService> logger)
    {
        _unitOfWork = unitOfWork;
        _emailService = emailService;
        _paymentService = paymentService;
        _logger = logger;
    }

    public async Task<OrderResult> CreateOrderAsync(CreateOrderRequest request)
    {
        await _unitOfWork.BeginTransactionAsync();
        
        try
        {
            // 1. Validate customer
            var customer = await _unitOfWork.Customers.GetByIdAsync(request.CustomerId);
            if (customer == null)
                throw new ArgumentException("Customer not found");

            // 2. Create order
            var order = new Order
            {
                CustomerId = request.CustomerId,
                ShippingAddress = request.ShippingAddress,
                ShippingCity = request.ShippingCity,
                ShippingZipCode = request.ShippingZipCode,
                OrderItems = new List<OrderItem>()
            };

            decimal totalAmount = 0;

            // 3. Process order items and update inventory
            foreach (var item in request.Items)
            {
                var product = await _unitOfWork.Products.GetByIdAsync(item.ProductId);
                if (product == null)
                    throw new ArgumentException($"Product {item.ProductId} not found");

                if (product.StockQuantity < item.Quantity)
                    throw new InvalidOperationException($"Insufficient stock for product {product.Name}");

                // Update stock
                await _unitOfWork.Products.UpdateProductStockAsync(product.Id, -item.Quantity);

                // Create order item
                var orderItem = new OrderItem
                {
                    ProductId = product.Id,
                    Quantity = item.Quantity,
                    UnitPrice = product.CurrentPrice,
                    TotalPrice = product.CurrentPrice * item.Quantity
                };

                order.OrderItems.Add(orderItem);
                totalAmount += orderItem.TotalPrice;
            }

            order.TotalAmount = totalAmount;
            await _unitOfWork.Orders.AddAsync(order);

            // 4. Process payment
            var paymentResult = await _paymentService.ProcessPaymentAsync(new PaymentRequest
            {
                OrderId = order.Id,
                Amount = totalAmount,
                PaymentMethod = request.PaymentMethod
            });

            if (!paymentResult.Success)
                throw new InvalidOperationException($"Payment failed: {paymentResult.ErrorMessage}");

            // 5. Save all changes
            await _unitOfWork.CommitAsync();
            await _unitOfWork.CommitTransactionAsync();

            // 6. Send confirmation email
            await _emailService.SendOrderConfirmationAsync(customer.Email, order);

            _logger.LogInformation("Order {OrderId} created successfully for customer {CustomerId}", 
                order.Id, customer.Id);

            return new OrderResult 
            { 
                Success = true, 
                OrderId = order.Id,
                OrderNumber = order.OrderNumber,
                TotalAmount = totalAmount
            };
        }
        catch (Exception ex)
        {
            await _unitOfWork.RollbackAsync();
            _logger.LogError(ex, "Error creating order for customer {CustomerId}", request.CustomerId);
            
            return new OrderResult 
            { 
                Success = false, 
                ErrorMessage = ex.Message 
            };
        }
    }

    public async Task<Order> ProcessOrderAsync(int orderId)
    {
        var order = await _unitOfWork.Orders.GetOrderWithDetailsAsync(orderId);
        if (order == null)
            throw new ArgumentException("Order not found");

        if (order.Status != OrderStatus.Pending)
            throw new InvalidOperationException("Order is not in pending status");

        // Validate stock availability
        foreach (var item in order.OrderItems)
        {
            if (item.Product.StockQuantity < item.Quantity)
            {
                order.Status = OrderStatus.Cancelled;
                await _unitOfWork.CommitAsync();
                
                throw new InvalidOperationException(
                    $"Insufficient stock for product {item.Product.Name}");
            }
        }

        order.Status = OrderStatus.Confirmed;
        await _unitOfWork.CommitAsync();

        _logger.LogInformation("Order {OrderId} processed successfully", orderId);
        return order;
    }}
  

Advanced Controller Implementation

  
    // API/Controllers/OrdersController.cs[ApiController][Route("api/[controller]")][Authorize]public class OrdersController : ControllerBase{
    private readonly IOrderService _orderService;
    private readonly IUnitOfWork _unitOfWork;
    private readonly IMapper _mapper;

    public OrdersController(
        IOrderService orderService,
        IUnitOfWork unitOfWork,
        IMapper mapper)
    {
        _orderService = orderService;
        _unitOfWork = unitOfWork;
        _mapper = mapper;
    }

    [HttpPost]
    public async Task<ActionResult<OrderResponse>> CreateOrder(CreateOrderRequest request)
    {
        try
        {
            var result = await _orderService.CreateOrderAsync(request);
            
            if (!result.Success)
                return BadRequest(new { error = result.ErrorMessage });

            var order = await _unitOfWork.Orders.GetOrderWithDetailsAsync(result.OrderId);
            var response = _mapper.Map<OrderResponse>(order);
            
            return CreatedAtAction(nameof(GetOrder), new { id = result.OrderId }, response);
        }
        catch (Exception ex)
        {
            return StatusCode(500, new { error = "An error occurred while creating the order" });
        }
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<OrderResponse>> GetOrder(int id)
    {
        var order = await _unitOfWork.Orders.GetOrderWithDetailsAsync(id);
        if (order == null)
            return NotFound();

        var response = _mapper.Map<OrderResponse>(order);
        return Ok(response);
    }

    [HttpGet("customer/{customerId}")]
    public async Task<ActionResult<IEnumerable<OrderResponse>>> GetCustomerOrders(int customerId)
    {
        var orders = await _unitOfWork.Orders.GetOrdersByCustomerAsync(customerId);
        var response = _mapper.Map<List<OrderResponse>>(orders);
        
        return Ok(response);
    }

    [HttpPut("{id}/status")]
    [Authorize(Roles = "Admin,Manager")]
    public async Task<IActionResult> UpdateOrderStatus(int id, UpdateOrderStatusRequest request)
    {
        try
        {
            await _orderService.UpdateOrderStatusAsync(id, request.Status);
            return NoContent();
        }
        catch (ArgumentException ex)
        {
            return NotFound(new { error = ex.Message });
        }
        catch (InvalidOperationException ex)
        {
            return BadRequest(new { error = ex.Message });
        }
    }}
  

6. Advanced Implementation Scenarios

Caching Repository Decorator

  
    // Infrastructure/Data/Decorators/CachedProductRepository.cspublic class CachedProductRepository : IProductRepository{
    private readonly IProductRepository _decorated;
    private readonly IDistributedCache _cache;
    private readonly ILogger<CachedProductRepository> _logger;

    public CachedProductRepository(
        IProductRepository decorated,
        IDistributedCache cache,
        ILogger<CachedProductRepository> logger)
    {
        _decorated = decorated;
        _cache = cache;
        _logger = logger;
    }

    public async Task<Product> GetByIdAsync(int id)
    {
        var cacheKey = $"product_{id}";
        
        try
        {
            var cachedProduct = await _cache.GetStringAsync(cacheKey);
            if (cachedProduct != null)
            {
                _logger.LogDebug("Cache hit for product {ProductId}", id);
                return JsonSerializer.Deserialize<Product>(cachedProduct);
            }

            var product = await _decorated.GetByIdAsync(id);
            if (product != null)
            {
                var options = new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30)
                };
                
                await _cache.SetStringAsync(cacheKey, 
                    JsonSerializer.Serialize(product), options);
            }

            return product;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error accessing cache for product {ProductId}", id);
            // Fall back to decorated repository
            return await _decorated.GetByIdAsync(id);
        }
    }

    public async Task<T> AddAsync(T entity)
    {
        var result = await _decorated.AddAsync(entity);
        await InvalidateCacheForProduct(result.Id);
        return result;
    }

    public async Task UpdateAsync(T entity)
    {
        await _decorated.UpdateAsync(entity);
        await InvalidateCacheForProduct(entity.Id);
    }

    public async Task DeleteAsync(T entity)
    {
        await _decorated.DeleteAsync(entity);
        await InvalidateCacheForProduct(entity.Id);
    }

    private async Task InvalidateCacheForProduct(int productId)
    {
        var cacheKey = $"product_{productId}";
        await _cache.RemoveAsync(cacheKey);
        
        // Also remove related cache entries
        await _cache.RemoveAsync("products_active");
        await _cache.RemoveAsync("products_featured");
    }}
  

Specification Pattern Integration

  
    // Core/Specifications/BaseSpecification.cspublic abstract class BaseSpecification<T> where T : BaseEntity{
    public Expression<Func<T, bool>> Criteria { get; }
    public List<Expression<Func<T, object>>> Includes { get; } = new();
    public List<string> IncludeStrings { get; } = new();
    public Expression<Func<T, object>> OrderBy { get; private set; }
    public Expression<Func<T, object>> OrderByDescending { get; private set; }
    public int Take { get; private set; }
    public int Skip { get; private set; }
    public bool IsPagingEnabled { get; private set; }

    protected BaseSpecification(Expression<Func<T, bool>> criteria)
    {
        Criteria = criteria;
    }

    protected virtual void AddInclude(Expression<Func<T, object>> includeExpression)
    {
        Includes.Add(includeExpression);
    }

    protected virtual void AddInclude(string includeString)
    {
        IncludeStrings.Add(includeString);
    }

    protected virtual void ApplyPaging(int skip, int take)
    {
        Skip = skip;
        Take = take;
        IsPagingEnabled = true;
    }

    protected virtual void ApplyOrderBy(Expression<Func<T, object>> orderByExpression)
    {
        OrderBy = orderByExpression;
    }

    protected virtual void ApplyOrderByDescending(Expression<Func<T, object>> orderByDescendingExpression)
    {
        OrderByDescending = orderByDescendingExpression;
    }}

// Core/Specifications/ProductsWithCategoryAndReviewsSpecification.cspublic class ProductsWithCategoryAndReviewsSpecification : BaseSpecification<Product>{
    public ProductsWithCategoryAndReviewsSpecification(ProductSpecParams productParams)
        : base(p => 
            (string.IsNullOrEmpty(productParams.Search) || 
             p.Name.Contains(productParams.Search) ||
             p.Description.Contains(productParams.Search)) &&
            (!productParams.CategoryId.HasValue || p.CategoryId == productParams.CategoryId) &&
            (!productParams.MinPrice.HasValue || p.Price >= productParams.MinPrice) &&
            (!productParams.MaxPrice.HasValue || p.Price <= productParams.MaxPrice) &&
            p.IsActive)
    {
        AddInclude(p => p.Category);
        AddInclude(p => p.Reviews);
        AddInclude(p => p.Inventory);

        if (!string.IsNullOrEmpty(productParams.Sort))
        {
            switch (productParams.Sort)
            {
                case "priceAsc":
                    ApplyOrderBy(p => p.Price);
                    break;
                case "priceDesc":
                    ApplyOrderByDescending(p => p.Price);
                    break;
                case "nameAsc":
                    ApplyOrderBy(p => p.Name);
                    break;
                case "nameDesc":
                    ApplyOrderByDescending(p => p.Name);
                    break;
                case "ratingDesc":
                    ApplyOrderByDescending(p => p.Reviews.Average(r => r.Rating));
                    break;
                default:
                    ApplyOrderBy(p => p.Name);
                    break;
            }
        }

        ApplyPaging(productParams.PageSize * (productParams.PageIndex - 1), productParams.PageSize);
    }

    public ProductsWithCategoryAndReviewsSpecification(int id) 
        : base(p => p.Id == id)
    {
        AddInclude(p => p.Category);
        AddInclude(p => p.Reviews);
        AddInclude(p => p.Inventory);
    }}
  

Generic Repository with Specification

  
    // Core/Interfaces/IGenericRepository.cspublic interface IGenericRepository<T> where T : BaseEntity{
    Task<T> GetByIdAsync(int id);
    Task<IReadOnlyList<T>> GetAllAsync();
    Task<T> GetEntityWithSpec(ISpecification<T> spec);
    Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec);
    Task<int> CountAsync(ISpecification<T> spec);
    Task<T> AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(T entity);}

// Infrastructure/Data/GenericRepository.cspublic class GenericRepository<T> : IGenericRepository<T> where T : BaseEntity{
    private readonly ApplicationDbContext _context;

    public GenericRepository(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<T> GetByIdAsync(int id)
    {
        return await _context.Set<T>().FindAsync(id);
    }

    public async Task<T> GetEntityWithSpec(ISpecification<T> spec)
    {
        return await ApplySpecification(spec).FirstOrDefaultAsync();
    }

    public async Task<IReadOnlyList<T>> ListAsync(ISpecification<T> spec)
    {
        return await ApplySpecification(spec).ToListAsync();
    }

    public async Task<int> CountAsync(ISpecification<T> spec)
    {
        return await ApplySpecification(spec, true).CountAsync();
    }

    public async Task<T> AddAsync(T entity)
    {
        await _context.Set<T>().AddAsync(entity);
        return entity;
    }

    public async Task UpdateAsync(T entity)
    {
        _context.Set<T>().Attach(entity);
        _context.Entry(entity).State = EntityState.Modified;
        await Task.CompletedTask;
    }

    public async Task DeleteAsync(T entity)
    {
        _context.Set<T>().Remove(entity);
        await Task.CompletedTask;
    }

    public async Task<IReadOnlyList<T>> GetAllAsync()
    {
        return await _context.Set<T>().ToListAsync();
    }

    private IQueryable<T> ApplySpecification(ISpecification<T> spec, bool forCount = false)
    {
        return SpecificationEvaluator<T>.GetQuery(_context.Set<T>().AsQueryable(), spec, forCount);
    }}
  

7. Testing Strategies

Unit Tests for Repository

  
    // Tests/Unit/Infrastructure/Repositories/ProductRepositoryTests.cspublic class ProductRepositoryTests{
    private readonly DbContextOptions<ApplicationDbContext> _dbContextOptions;
    private readonly ApplicationDbContext _context;
    private readonly ProductRepository _productRepository;

    public ProductRepositoryTests()
    {
        _dbContextOptions = new DbContextOptionsBuilder<ApplicationDbContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;

        _context = new ApplicationDbContext(_dbContextOptions);
        _productRepository = new ProductRepository(_context);
    }

    [Fact]
    public async Task GetByIdAsync_WhenProductExists_ReturnsProduct()
    {
        // Arrange
        var product = new Product 
        { 
            Id = 1, 
            Name = "Test Product", 
            Price = 99.99m,
            CategoryId = 1
        };
        await _context.Products.AddAsync(product);
        await _context.SaveChangesAsync();

        // Act
        var result = await _productRepository.GetByIdAsync(1);

        // Assert
        result.Should().NotBeNull();
        result.Id.Should().Be(1);
        result.Name.Should().Be("Test Product");
    }

    [Fact]
    public async Task GetProductsByCategoryAsync_WhenCategoryExists_ReturnsProducts()
    {
        // Arrange
        var category = new Category { Id = 1, Name = "Electronics" };
        var products = new List<Product>
        {
            new Product { Id = 1, Name = "Laptop", CategoryId = 1, IsActive = true },
            new Product { Id = 2, Name = "Phone", CategoryId = 1, IsActive = true },
            new Product { Id = 3, Name = "Tablet", CategoryId = 1, IsActive = false }
        };

        await _context.Categories.AddAsync(category);
        await _context.Products.AddRangeAsync(products);
        await _context.SaveChangesAsync();

        // Act
        var result = await _productRepository.GetProductsByCategoryAsync(1);

        // Assert
        result.Should().HaveCount(2);
        result.All(p => p.CategoryId == 1).Should().BeTrue();
        result.All(p => p.IsActive).Should().BeTrue();
    }

    [Fact]
    public async Task UpdateProductStockAsync_WithValidQuantity_UpdatesStock()
    {
        // Arrange
        var product = new Product 
        { 
            Id = 1, 
            Name = "Test Product", 
            StockQuantity = 50 
        };
        await _context.Products.AddAsync(product);
        await _context.SaveChangesAsync();

        // Act
        await _productRepository.UpdateProductStockAsync(1, -10);

        // Assert
        var updatedProduct = await _context.Products.FindAsync(1);
        updatedProduct.StockQuantity.Should().Be(40);
    }

    [Fact]
    public async Task UpdateProductStockAsync_WithInsufficientStock_ThrowsException()
    {
        // Arrange
        var product = new Product 
        { 
            Id = 1, 
            Name = "Test Product", 
            StockQuantity = 5 
        };
        await _context.Products.AddAsync(product);
        await _context.SaveChangesAsync();

        // Act & Assert
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            _productRepository.UpdateProductStockAsync(1, -10));
    }}
  

Integration Tests

  
    // Tests/Integration/OrderServiceIntegrationTests.cspublic class OrderServiceIntegrationTests : IClassFixture<CustomWebApplicationFactory>{
    private readonly CustomWebApplicationFactory _factory;
    private readonly IServiceScope _scope;
    private readonly IOrderService _orderService;
    private readonly IUnitOfWork _unitOfWork;
    private readonly ApplicationDbContext _context;

    public OrderServiceIntegrationTests(CustomWebApplicationFactory factory)
    {
        _factory = factory;
        _scope = _factory.Services.CreateScope();
        _orderService = _scope.ServiceProvider.GetRequiredService<IOrderService>();
        _unitOfWork = _scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
        _context = _scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
    }

    [Fact]
    public async Task CreateOrderAsync_WithValidRequest_CreatesOrderAndUpdatesInventory()
    {
        // Arrange
        var customer = new Customer { Name = "Test Customer", Email = "[email protected]" };
        var category = new Category { Name = "Electronics" };
        var products = new List<Product>
        {
            new Product { Name = "Laptop", Price = 1000m, StockQuantity = 10, Category = category },
            new Product { Name = "Mouse", Price = 25m, StockQuantity = 50, Category = category }
        };

        await _context.Customers.AddAsync(customer);
        await _context.Categories.AddAsync(category);
        await _context.Products.AddRangeAsync(products);
        await _context.SaveChangesAsync();

        var request = new CreateOrderRequest
        {
            CustomerId = customer.Id,
            ShippingAddress = "123 Test St",
            ShippingCity = "Test City",
            ShippingZipCode = "12345",
            Items = new List<OrderItemRequest>
            {
                new OrderItemRequest { ProductId = products[0].Id, Quantity = 1 },
                new OrderItemRequest { ProductId = products[1].Id, Quantity = 2 }
            },
            PaymentMethod = "CreditCard"
        };

        // Act
        var result = await _orderService.CreateOrderAsync(request);

        // Assert
        result.Success.Should().BeTrue();
        result.OrderId.Should().BeGreaterThan(0);

        var order = await _unitOfWork.Orders.GetOrderWithDetailsAsync(result.OrderId);
        order.Should().NotBeNull();
        order.TotalAmount.Should().Be(1050m); // 1000 + (25 * 2)
        order.Status.Should().Be(OrderStatus.Confirmed);

        // Verify inventory was updated
        var laptop = await _unitOfWork.Products.GetByIdAsync(products[0].Id);
        var mouse = await _unitOfWork.Products.GetByIdAsync(products[1].Id);
        
        laptop.StockQuantity.Should().Be(9); // 10 - 1
        mouse.StockQuantity.Should().Be(48); // 50 - 2
    }}
  

Mocking Dependencies for Service Tests

  
    // Tests/Unit/Application/Services/OrderServiceTests.cspublic class OrderServiceTests{
    private readonly Mock<IUnitOfWork> _mockUnitOfWork;
    private readonly Mock<IEmailService> _mockEmailService;
    private readonly Mock<IPaymentService> _mockPaymentService;
    private readonly Mock<ILogger<OrderService>> _mockLogger;
    private readonly OrderService _orderService;

    public OrderServiceTests()
    {
        _mockUnitOfWork = new Mock<IUnitOfWork>();
        _mockEmailService = new Mock<IEmailService>();
        _mockPaymentService = new Mock<IPaymentService>();
        _mockLogger = new Mock<ILogger<OrderService>>();

        _orderService = new OrderService(
            _mockUnitOfWork.Object,
            _mockEmailService.Object,
            _mockPaymentService.Object,
            _mockLogger.Object);
    }

    [Fact]
    public async Task CreateOrderAsync_WithValidRequest_ReturnsSuccess()
    {
        // Arrange
        var customer = new Customer { Id = 1, Email = "[email protected]" };
        var products = new List<Product>
        {
            new Product { Id = 1, Name = "Laptop", Price = 1000m, StockQuantity = 10 },
            new Product { Id = 2, Name = "Mouse", Price = 25m, StockQuantity = 50 }
        };

        var request = new CreateOrderRequest
        {
            CustomerId = 1,
            Items = new List<OrderItemRequest>
            {
                new OrderItemRequest { ProductId = 1, Quantity = 1 },
                new OrderItemRequest { ProductId = 2, Quantity = 2 }
            }
        };

        // Setup mocks
        _mockUnitOfWork.Setup(u => u.Customers.GetByIdAsync(1))
            .ReturnsAsync(customer);
        
        _mockUnitOfWork.Setup(u => u.Products.GetByIdAsync(1))
            .ReturnsAsync(products[0]);
            
        _mockUnitOfWork.Setup(u => u.Products.GetByIdAsync(2))
            .ReturnsAsync(products[1]);

        _mockPaymentService.Setup(p => p.ProcessPaymentAsync(It.IsAny<PaymentRequest>()))
            .ReturnsAsync(new PaymentResult { Success = true });

        _mockUnitOfWork.Setup(u => u.CommitAsync())
            .ReturnsAsync(1);

        // Act
        var result = await _orderService.CreateOrderAsync(request);

        // Assert
        result.Success.Should().BeTrue();
        
        _mockUnitOfWork.Verify(u => u.BeginTransactionAsync(), Times.Once);
        _mockUnitOfWork.Verify(u => u.CommitTransactionAsync(), Times.Once);
        _mockEmailService.Verify(e => e.SendOrderConfirmationAsync(
            "[email protected]", It.IsAny<Order>()), Times.Once);
    }}
  

8. Performance Optimization

Query Optimization Techniques

  
    // Infrastructure/Data/Repositories/OptimizedProductRepository.cspublic class OptimizedProductRepository : IProductRepository{
    private readonly ApplicationDbContext _context;

    public OptimizedProductRepository(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<IReadOnlyList<Product>> GetActiveProductsWithDetailsAsync()
    {
        // Use AsNoTracking for read-only operations
        return await _context.Products
            .AsNoTracking() // Improves performance for read-only
            .Where(p => p.IsActive)
            .Include(p => p.Category)
            .Include(p => p.Inventory)
            .Include(p => p.Reviews)
            .Select(p => new Product // Use projection to load only needed fields
            {
                Id = p.Id,
                Name = p.Name,
                Price = p.Price,
                DiscountPrice = p.DiscountPrice,
                ImageUrl = p.ImageUrl,
                Category = new Category { Name = p.Category.Name },
                Reviews = p.Reviews.Select(r => new Review 
                { 
                    Rating = r.Rating,
                    Comment = r.Comment 
                }).ToList(),
                StockQuantity = p.Inventory.Quantity
            })
            .OrderBy(p => p.Category.Name)
            .ThenBy(p => p.Name)
            .ToListAsync();
    }

    public async Task<IReadOnlyList<Product>> SearchProductsAsync(string searchTerm, int pageNumber, int pageSize)
    {
        // Use compiled query for frequently executed queries
        var compiledQuery = EF.CompileAsyncQuery(
            (ApplicationDbContext context, string term, int skip, int take) =>
                context.Products
                    .AsNoTracking()
                    .Where(p => p.IsActive && 
                               (p.Name.Contains(term) || 
                                p.Description.Contains(term)))
                    .Include(p => p.Category)
                    .OrderBy(p => p.Name)
                    .Skip(skip)
                    .Take(take)
                    .ToList());

        return await compiledQuery(_context, searchTerm, (pageNumber - 1) * pageSize, pageSize);
    }}
  

Bulk Operations

  
    // Infrastructure/Data/Repositories/BulkOperationsRepository.cspublic class BulkOperationsRepository : IBulkOperationsRepository{
    private readonly ApplicationDbContext _context;

    public BulkOperationsRepository(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task BulkInsertProductsAsync(IEnumerable<Product> products)
    {
        // Use EF Core Bulk Extensions for large inserts
        await _context.BulkInsertAsync(products, options =>
        {
            options.BatchSize = 1000;
            options.UseTempDB = true;
        });
    }

    public async Task BulkUpdateProductPricesAsync(IEnumerable<ProductPriceUpdate> updates)
    {
        // Use raw SQL for bulk updates
        var updateQuery = @"UPDATE Products 
                           SET Price = @Price, UpdatedAt = GETUTCDATE() 
                           WHERE Id = @Id";

        foreach (var update in updates.Batch(1000)) // Process in batches
        {
            foreach (var item in update)
            {
                await _context.Database.ExecuteSqlRawAsync(
                    updateQuery, 
                    new SqlParameter("@Price", item.Price),
                    new SqlParameter("@Id", item.ProductId));
            }
        }
    }}
  

9. Common Pitfalls & Best Practices

Common Pitfalls

  
    // ❌ COMMON MISTAKES

// 1. Not handling transactions properlypublic async Task ProcessOrder(Order order){
    // Missing transaction scope
    await UpdateInventory(order);
    await _orderRepository.AddAsync(order);
    await _paymentRepository.AddAsync(payment);
    // If payment fails, inventory is already updated!}

// 2. Repository returning IQueryable (leaks abstraction)public interface IProductRepository{
    IQueryable<Product> GetAll(); // ❌ Don't do this!}

// 3. Not implementing proper disposalpublic class ProductService{
    private readonly IUnitOfWork _unitOfWork;
    
    public ProductService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    // Missing IDisposable implementation}

// 4. Too many database round trips (N+1 problem)public async Task ProcessOrders(){
    var orders = await _orderRepository.GetAllAsync();
    
    foreach (var order in orders)
    {
        // This causes N+1 queries!
        var customer = await _customerRepository.GetByIdAsync(order.CustomerId);
        ProcessOrder(order, customer);
    }}
  

Best Practices Implementation

  
    // ✅ BEST PRACTICES

// 1. Proper transaction handlingpublic async Task<OrderResult> ProcessOrderAsync(Order order){
    await _unitOfWork.BeginTransactionAsync();
    
    try
    {
        await UpdateInventory(order);
        await _unitOfWork.Orders.AddAsync(order);
        await ProcessPayment(order);
        
        await _unitOfWork.CommitAsync();
        await _unitOfWork.CommitTransactionAsync();
        
        return OrderResult.Success(order.Id);
    }
    catch (Exception ex)
    {
        await _unitOfWork.RollbackAsync();
        _logger.LogError(ex, "Order processing failed");
        return OrderResult.Failure(ex.Message);
    }}

// 2. Use specific repository methods instead of IQueryablepublic interface IOrderRepository : IRepository<Order>{
    // ✅ Specific, meaningful methods
    Task<Order> GetOrderWithCustomerAndItemsAsync(int orderId);
    Task<IReadOnlyList<Order>> GetRecentOrdersAsync(int count);
    Task<IReadOnlyList<Order>> GetOrdersByStatusAsync(OrderStatus status);}

// 3. Implement proper disposal patternpublic class ProductService : IDisposable{
    private readonly IUnitOfWork _unitOfWork;
    private bool _disposed = false;

    public ProductService(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed && disposing)
        {
            _unitOfWork?.Dispose();
        }
        _disposed = true;
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }}

// 4. Use eager loading and batching to avoid N+1public async Task ProcessOrders(){
    // Load all orders with customers in one query
    var orders = await _orderRepository.GetOrdersWithCustomersAsync();
    
    // Process in memory
    foreach (var order in orders)
    {
        ProcessOrder(order, order.Customer); // Customer already loaded
    }}
  

10. Alternatives & When to Use What

CQRS Pattern Alternative

  
    // Core/CQRS/Commands/CreateProductCommand.cspublic class CreateProductCommand : IRequest<CommandResult>{
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
    public int StockQuantity { get; set; }}

// Core/CQRS/Handlers/CreateProductCommandHandler.cspublic class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, CommandResult>{
    private readonly IUnitOfWork _unitOfWork;
    private readonly ILogger<CreateProductCommandHandler> _logger;

    public CreateProductCommandHandler(
        IUnitOfWork unitOfWork,
        ILogger<CreateProductCommandHandler> logger)
    {
        _unitOfWork = unitOfWork;
        _logger = logger;
    }

    public async Task<CommandResult> Handle(CreateProductCommand request, CancellationToken cancellationToken)
    {
        try
        {
            var product = new Product
            {
                Name = request.Name,
                Description = request.Description,
                Price = request.Price,
                CategoryId = request.CategoryId,
                StockQuantity = request.StockQuantity
            };

            await _unitOfWork.Products.AddAsync(product);
            await _unitOfWork.CommitAsync();

            _logger.LogInformation("Product {ProductId} created successfully", product.Id);

            return CommandResult.Success(product.Id);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error creating product");
            return CommandResult.Failure(ex.Message);
        }
    }}

// Core/CQRS/Queries/GetProductQuery.cspublic class GetProductQuery : IRequest<ProductDto>{
    public int ProductId { get; set; }}

// Core/CQRS/Handlers/GetProductQueryHandler.cspublic class GetProductQueryHandler : IRequestHandler<GetProductQuery, ProductDto>{
    private readonly IReadOnlyProductRepository _readRepository;

    public GetProductQueryHandler(IReadOnlyProductRepository readRepository)
    {
        _readRepository = readRepository;
    }

    public async Task<ProductDto> Handle(GetProductQuery request, CancellationToken cancellationToken)
    {
        return await _readRepository.GetProductDtoAsync(request.ProductId);
    }}
  

When to Use Repository vs CQRS vs Direct DbContext

PatternUse CaseProsCons
Repository + UoWMedium complexity apps, team consistency, testing focusGood abstraction, testable, consistent data accessCan be overkill for simple apps, some abstraction leakage
CQRSHigh-performance apps, complex read/write requirementsScalable, optimized queries, clear separationMore complex, eventual consistency challenges
Direct DbContextSimple CRUD apps, prototypes, small teamsFast development, full EF powerHard to test, business logic in controllers
Generic RepositoryRapid development, consistent basic operationsReduces boilerplate, consistent interfaceLimited flexibility for complex queries

Decision Framework

  
    public class PatternSelectionService{
    public DataAccessPattern SelectPattern(ApplicationRequirements requirements)
    {
        return requirements switch
        {
            { Complexity: Complexity.Simple, TeamSize: < 3 } 
                => DataAccessPattern.DirectDbContext,
                
            { Complexity: Complexity.Medium, NeedsTesting: true } 
                => DataAccessPattern.RepositoryUnitOfWork,
                
            { Complexity: Complexity.High, ReadWriteRatio: > 5 } 
                => DataAccessPattern.CQRS,
                
            { NeedsRapidDevelopment: true, ConsistencyImportant: false } 
                => DataAccessPattern.GenericRepository,
                
            _ => DataAccessPattern.RepositoryUnitOfWork
        };
    }}

public enum DataAccessPattern{
    DirectDbContext,
    RepositoryUnitOfWork,
    CQRS,
    GenericRepository
}

public class ApplicationRequirements{
    public Complexity Complexity { get; set; }
    public int TeamSize { get; set; }
    public bool NeedsTesting { get; set; }
    public double ReadWriteRatio { get; set; }
    public bool NeedsRapidDevelopment { get; set; }
    public bool ConsistencyImportant { get; set; }}
  

🎯 Conclusion

The Repository and Unit of Work patterns provide a robust foundation for building maintainable, testable, and scalable  ASP.NET  Core applications. While they introduce some complexity, the benefits in terms of testability, maintainability, and team consistency make them invaluable for enterprise applications.

Key Takeaways

  • Use these patterns when building applications that require testing and maintainability

  • Implement proper transaction handling for complex operations

  • Consider alternatives like CQRS for high-performance scenarios

  • Always follow best practices for disposal and error handling

  • Use the pattern that best fits your application's complexity and team size

Remember, patterns are tools - use them wisely based on your specific requirements rather than applying them blindly.

This comprehensive guide provides everything needed to implement Repository and Unit of Work patterns effectively in  ASP.NET  Core applications, from basic concepts to advanced production-ready implementations.