ASP.NET Core  

ASP.NET Core Architecture Patterns: CQRS, DDD, Clean Architecture Mastery (Part -19 of 40)

image

Previous article: ASP.NET Core Design Patterns Revolution: Repository, Singleton & Clean Architecture (Part-18 of 40)

Table of Contents

  1. Introduction to Architectural Patterns

  2. Layered Architecture

  3. Clean Architecture & Onion Architecture

  4. Domain-Driven Design (DDD)

  5. Command Query Responsibility Segregation (CQRS)

  6. Event Sourcing

  7. Microservices Architecture

  8. Vertical Slice Architecture

  9. Repository Pattern & Unit of Work

  10. Real-World E-Commerce Implementation

  11. Testing Strategies

  12. Best Practices & Anti-Patterns

1. Introduction to Architectural Patterns

Why Architecture Matters?

Architecture is the foundation that determines your application's scalability, maintainability, and success. Poor architecture leads to:

  • Spaghetti Code : Unmaintainable, tangled dependencies

  • Rigid Systems : Difficult to change or extend

  • Performance Bottlenecks : Poor scalability under load

  • High Technical Debt : Costly rewrites and fixes

Real-Life Analogy: City Planning

Think of software architecture like city planning:

  • Monolithic  = Small town with everything in one place

  • Layered  = Organized city with residential, commercial zones

  • Microservices  = Metropolitan area with specialized districts

  • DDD  = Planning based on community needs and boundaries

Evolution of  ASP.NET  Core Architecture

  
    // Traditional N-Tier (2000s)
Presentation → Business Logic → Data Access

// Layered Architecture (2010s)
UI → Service Layer → Repository → Database

// Modern Patterns (2020s)
Clean Architecture, CQRS, DDD, Microservices
  

2. Layered Architecture

Traditional 3-Tier Architecture

  
    // Project Structure
ECommerceApp/
├── ECommerceApp.Web/          // Presentation Layer
├── ECommerceApp.Services/     // Business Logic Layer  
├── ECommerceApp.Data/         // Data Access Layer
└── ECommerceApp.Models/       // Shared Models
  

Implementation Example

  
    // Data Layer - Repository
public interface IProductRepository
{
    Task<Product> GetByIdAsync(int id);
    Task<List<Product>> GetAllAsync();
    Task AddAsync(Product product);
    Task UpdateAsync(Product product);
    Task DeleteAsync(int id);
}

public class ProductRepository : IProductRepository
{
    private readonly ApplicationDbContext _context;

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

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

    public async Task<List<Product>> GetAllAsync()
    {
        return await _context.Products.ToListAsync();
    }

    public async Task AddAsync(Product product)
    {
        await _context.Products.AddAsync(product);
        await _context.SaveChangesAsync();
    }

    // Other implementations...
}
  

Service Layer

  
    // Business Logic Layer
public interface IProductService
{
    Task<ProductDto> GetProductAsync(int id);
    Task<List<ProductDto>> GetProductsAsync();
    Task CreateProductAsync(CreateProductRequest request);
    Task UpdateProductAsync(UpdateProductRequest request);
    Task DeleteProductAsync(int id);
}

public class ProductService : IProductService
{
    private readonly IProductRepository _productRepository;
    private readonly IMapper _mapper;

    public ProductService(IProductRepository productRepository, IMapper mapper)
    {
        _productRepository = productRepository;
        _mapper = mapper;
    }

    public async Task<ProductDto> GetProductAsync(int id)
    {
        var product = await _productRepository.GetByIdAsync(id);
        if (product == null)
            throw new NotFoundException($"Product with ID {id} not found");
        
        return _mapper.Map<ProductDto>(product);
    }

    public async Task CreateProductAsync(CreateProductRequest request)
    {
        var product = new Product
        {
            Name = request.Name,
            Description = request.Description,
            Price = request.Price,
            StockQuantity = request.StockQuantity,
            CreatedAt = DateTime.UtcNow
        };

        await _productRepository.AddAsync(product);
    }
}
  

Controller Layer

  
    // Presentation Layer
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<ProductDto>> GetProduct(int id)
    {
        try
        {
            var product = await _productService.GetProductAsync(id);
            return Ok(product);
        }
        catch (NotFoundException ex)
        {
            return NotFound(ex.Message);
        }
    }

    [HttpPost]
    public async Task<ActionResult> CreateProduct(CreateProductRequest request)
    {
        await _productService.CreateProductAsync(request);
        return CreatedAtAction(nameof(GetProduct), new { id = request.Id }, null);
    }
}
  

Pros and Cons of Layered Architecture

Pros

  • Simple to understand and implement

  • Clear separation of concerns

  • Easy to test individual layers

Cons

  • Tight coupling between layers

  • Business logic often leaks into multiple layers

  • Difficult to change database technology

  • Can lead to anemic domain models

3. Clean Architecture & Onion Architecture

Clean Architecture Principles

Clean Architecture emphasizes

  • Independence from Frameworks : Not dependent on UI, database, or external agencies

  • Testable : Business rules can be tested without UI, database, and web server

  • Independence from UI : UI can change easily without changing business rules

  • Independence from Database : Business rules not bound to the database

  • Independence from External Agencies : Business rules don't know anything about outside interfaces

Onion Architecture Layers

  
    Domain Layer (Core)
        /         \
Application Layer  Infrastructure Layer
        \         /
     Presentation Layer (UI)
  

Project Structure

  
    ECommerceClean/
├── ECommerceClean.Domain/          # Core business logic
├── ECommerceClean.Application/     # Use cases & application logic  
├── ECommerceClean.Infrastructure/  # External concerns
├── ECommerceClean.Web/            # Presentation layer
└── ECommerceClean.Tests/          # Test projects
  

Domain Layer (Core)

  
    // Domain Entities
public class Product : Entity, IAggregateRoot
{
    public string Name { get; private set; }
    public string Description { get; private set; }
    public decimal Price { get; private set; }
    public int StockQuantity { get; private set; }
    public DateTime CreatedAt { get; private set; }
    public DateTime? UpdatedAt { get; private set; }

    // Private constructor for EF
    private Product() { }

    public Product(string name, string description, decimal price, int stockQuantity)
    {
        Name = name ?? throw new ArgumentNullException(nameof(name));
        Description = description;
        Price = price > 0 ? price : throw new ArgumentException("Price must be positive");
        StockQuantity = stockQuantity;
        CreatedAt = DateTime.UtcNow;
    }

    // Domain methods
    public void UpdatePrice(decimal newPrice)
    {
        if (newPrice <= 0)
            throw new DomainException("Price must be positive");
        
        Price = newPrice;
        UpdatedAt = DateTime.UtcNow;
    }

    public void IncreaseStock(int quantity)
    {
        if (quantity <= 0)
            throw new DomainException("Quantity must be positive");
        
        StockQuantity += quantity;
        UpdatedAt = DateTime.UtcNow;
    }

    public void DecreaseStock(int quantity)
    {
        if (quantity <= 0)
            throw new DomainException("Quantity must be positive");
        
        if (StockQuantity < quantity)
            throw new DomainException("Insufficient stock");
        
        StockQuantity -= quantity;
        UpdatedAt = DateTime.UtcNow;
    }
}

// Base Entity
public abstract class Entity
{
    public int Id { get; protected set; }

    private readonly List<IDomainEvent> _domainEvents = new();
    public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();

    protected void AddDomainEvent(IDomainEvent eventItem)
    {
        _domainEvents.Add(eventItem);
    }

    public void ClearDomainEvents()
    {
        _domainEvents.Clear();
    }
}

// Domain Events
public interface IDomainEvent
{
    DateTime OccurredOn { get; }
}

public class ProductPriceChangedEvent : IDomainEvent
{
    public int ProductId { get; }
    public decimal OldPrice { get; }
    public decimal NewPrice { get; }
    public DateTime OccurredOn { get; }

    public ProductPriceChangedEvent(int productId, decimal oldPrice, decimal newPrice)
    {
        ProductId = productId;
        OldPrice = oldPrice;
        NewPrice = newPrice;
        OccurredOn = DateTime.UtcNow;
    }
}
  

Application Layer

  
    // Commands and Queries
public static class ProductCommands
{
    public record CreateProductCommand(string Name, string Description, decimal Price, int StockQuantity) : IRequest<int>;
    
    public record UpdateProductPriceCommand(int ProductId, decimal NewPrice) : IRequest;
    
    public record DeleteProductCommand(int ProductId) : IRequest;
}

public static class ProductQueries
{
    public record GetProductQuery(int ProductId) : IRequest<ProductDto>;
    
    public record GetProductsQuery(int PageNumber = 1, int PageSize = 10) : IRequest<PagedList<ProductDto>>;
}

// Command Handlers
public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, int>
{
    private readonly IProductRepository _productRepository;

    public CreateProductCommandHandler(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public async Task<int> Handle(CreateProductCommand request, CancellationToken cancellationToken)
    {
        var product = new Product(request.Name, request.Description, request.Price, request.StockQuantity);
        
        await _productRepository.AddAsync(product);
        
        return product.Id;
    }
}

public class UpdateProductPriceCommandHandler : IRequestHandler<UpdateProductPriceCommand>
{
    private readonly IProductRepository _productRepository;

    public UpdateProductPriceCommandHandler(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public async Task Handle(UpdateProductPriceCommand request, CancellationToken cancellationToken)
    {
        var product = await _productRepository.GetByIdAsync(request.ProductId);
        if (product == null)
            throw new NotFoundException($"Product with ID {request.ProductId} not found");

        var oldPrice = product.Price;
        product.UpdatePrice(request.NewPrice);
        
        await _productRepository.UpdateAsync(product);
    }
}

// Query Handlers
public class GetProductQueryHandler : IRequestHandler<GetProductQuery, ProductDto>
{
    private readonly IProductRepository _productRepository;
    private readonly IMapper _mapper;

    public GetProductQueryHandler(IProductRepository productRepository, IMapper mapper)
    {
        _productRepository = productRepository;
        _mapper = mapper;
    }

    public async Task<ProductDto> Handle(GetProductQuery request, CancellationToken cancellationToken)
    {
        var product = await _productRepository.GetByIdAsync(request.ProductId);
        if (product == null)
            throw new NotFoundException($"Product with ID {request.ProductId} not found");

        return _mapper.Map<ProductDto>(product);
    }
}
  

Infrastructure Layer

  
    // Repository Implementation
public class ProductRepository : IProductRepository
{
    private readonly ApplicationDbContext _context;

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

    public async Task<Product> GetByIdAsync(int id)
    {
        return await _context.Products
            .FirstOrDefaultAsync(p => p.Id == id);
    }

    public async Task AddAsync(Product product)
    {
        await _context.Products.AddAsync(product);
        await _context.SaveChangesAsync();
    }

    public async Task UpdateAsync(Product product)
    {
        _context.Products.Update(product);
        await _context.SaveChangesAsync();
    }
}

// DbContext
public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) 
        : base(options)
    {
    }

    public DbSet<Product> Products { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
    }
}

// Entity Configuration
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
    public void Configure(EntityTypeBuilder<Product> builder)
    {
        builder.HasKey(p => p.Id);
        
        builder.Property(p => p.Name)
            .IsRequired()
            .HasMaxLength(100);
            
        builder.Property(p => p.Description)
            .HasMaxLength(500);
            
        builder.Property(p => p.Price)
            .HasColumnType("decimal(18,2)");
            
        builder.Property(p => p.StockQuantity)
            .IsRequired();
            
        builder.Property(p => p.CreatedAt)
            .IsRequired();
    }
}
  

Presentation Layer

  
    // Minimal API Endpoints
public static class ProductEndpoints
{
    public static void MapProductEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("api/products");
        
        group.MapGet("/{id}", GetProduct)
            .WithName("GetProduct")
            .Produces<ProductDto>(StatusCodes.Status200OK)
            .Produces(StatusCodes.Status404NotFound);
            
        group.MapPost("/", CreateProduct)
            .Produces<int>(StatusCodes.Status201Created)
            .Produces(StatusCodes.Status400BadRequest);
            
        group.MapPut("/{id}/price", UpdateProductPrice)
            .Produces(StatusCodes.Status204NoContent)
            .Produces(StatusCodes.Status404NotFound);
    }

    private static async Task<IResult> GetProduct(
        int id,
        IMediator mediator,
        CancellationToken cancellationToken)
    {
        try
        {
            var query = new GetProductQuery(id);
            var product = await mediator.Send(query, cancellationToken);
            return Results.Ok(product);
        }
        catch (NotFoundException ex)
        {
            return Results.NotFound(ex.Message);
        }
    }

    private static async Task<IResult> CreateProduct(
        CreateProductCommand command,
        IMediator mediator,
        CancellationToken cancellationToken)
    {
        try
        {
            var productId = await mediator.Send(command, cancellationToken);
            return Results.CreatedAtRoute("GetProduct", new { id = productId }, productId);
        }
        catch (DomainException ex)
        {
            return Results.BadRequest(ex.Message);
        }
    }

    private static async Task<IResult> UpdateProductPrice(
        int id,
        UpdateProductPriceRequest request,
        IMediator mediator,
        CancellationToken cancellationToken)
    {
        try
        {
            var command = new UpdateProductPriceCommand(id, request.NewPrice);
            await mediator.Send(command, cancellationToken);
            return Results.NoContent();
        }
        catch (NotFoundException ex)
        {
            return Results.NotFound(ex.Message);
        }
        catch (DomainException ex)
        {
            return Results.BadRequest(ex.Message);
        }
    }
}
  

Dependency Injection Setup

  
    // Program.cs setup
var builder = WebApplication.CreateBuilder(args);

// Add layers
builder.Services.AddDomain();
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);
builder.Services.AddPresentation();

var app = builder.Build();

// Configure pipeline
app.UseInfrastructure();
app.MapProductEndpoints();

app.Run();

// Extension methods for DI
public static class DependencyInjection
{
    public static IServiceCollection AddDomain(this IServiceCollection services)
    {
        // Domain services registration
        return services;
    }

    public static IServiceCollection AddApplication(this IServiceCollection services)
    {
        services.AddMediatR(cfg => 
            cfg.RegisterServicesFromAssembly(typeof(CreateProductCommand).Assembly));
        
        services.AddScoped(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
        services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
        
        services.AddAutoMapper(typeof(ProductProfile).Assembly);
        
        return services;
    }

    public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
    {
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
            
        services.AddScoped<IProductRepository, ProductRepository>();
        
        return services;
    }

    public static IServiceCollection AddPresentation(this IServiceCollection services)
    {
        services.AddControllers();
        services.AddEndpointsApiExplorer();
        services.AddSwaggerGen();
        
        return services;
    }
}
  

4. Domain-Driven Design (DDD)

DDD Building Blocks

Entities

  
    public class Order : Entity, IAggregateRoot
{
    private readonly List<OrderItem> _orderItems = new();
    
    public int CustomerId { get; private set; }
    public OrderStatus Status { get; private set; }
    public decimal TotalAmount { get; private set; }
    public DateTime CreatedAt { get; private set; }
    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();

    private Order() { }

    public Order(int customerId)
    {
        CustomerId = customerId;
        Status = OrderStatus.Pending;
        CreatedAt = DateTime.UtcNow;
    }

    public void AddItem(int productId, string productName, decimal unitPrice, int quantity)
    {
        var existingItem = _orderItems.FirstOrDefault(item => item.ProductId == productId);
        
        if (existingItem != null)
        {
            existingItem.IncreaseQuantity(quantity);
        }
        else
        {
            var orderItem = new OrderItem(productId, productName, unitPrice, quantity);
            _orderItems.Add(orderItem);
        }
        
        UpdateTotalAmount();
    }

    public void RemoveItem(int productId)
    {
        var item = _orderItems.FirstOrDefault(item => item.ProductId == productId);
        if (item != null)
        {
            _orderItems.Remove(item);
            UpdateTotalAmount();
        }
    }

    public void CompleteOrder()
    {
        if (Status != OrderStatus.Pending)
            throw new DomainException("Only pending orders can be completed");
            
        if (!_orderItems.Any())
            throw new DomainException("Cannot complete empty order");
            
        Status = OrderStatus.Completed;
        AddDomainEvent(new OrderCompletedEvent(Id, CustomerId, TotalAmount));
    }

    public void CancelOrder()
    {
        if (Status == OrderStatus.Completed || Status == OrderStatus.Shipped)
            throw new DomainException("Cannot cancel completed or shipped order");
            
        Status = OrderStatus.Cancelled;
        AddDomainEvent(new OrderCancelledEvent(Id, CustomerId));
    }

    private void UpdateTotalAmount()
    {
        TotalAmount = _orderItems.Sum(item => item.UnitPrice * item.Quantity);
    }
}
  

Value Objects

  
    public record Address : ValueObject
{
    public string Street { get; }
    public string City { get; }
    public string State { get; }
    public string Country { get; }
    public string ZipCode { get; }

    public Address(string street, string city, string state, string country, string zipCode)
    {
        Street = street ?? throw new ArgumentNullException(nameof(street));
        City = city ?? throw new ArgumentNullException(nameof(city));
        State = state ?? throw new ArgumentNullException(nameof(state));
        Country = country ?? throw new ArgumentNullException(nameof(country));
        ZipCode = zipCode ?? throw new ArgumentNullException(nameof(zipCode));
    }

    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return Street;
        yield return City;
        yield return State;
        yield return Country;
        yield return ZipCode;
    }
}

public abstract record ValueObject
{
    protected abstract IEnumerable<object> GetEqualityComponents();

    public override int GetHashCode()
    {
        return GetEqualityComponents()
            .Select(x => x?.GetHashCode() ?? 0)
            .Aggregate((x, y) => x ^ y);
    }

    public virtual bool Equals(ValueObject? other)
    {
        if (other is null) return false;

        return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
    }
}
  

Aggregates

  
    public class OrderItem : Entity
{
    public int ProductId { get; private set; }
    public string ProductName { get; private set; }
    public decimal UnitPrice { get; private set; }
    public int Quantity { get; private set; }

    private OrderItem() { }

    public OrderItem(int productId, string productName, decimal unitPrice, int quantity)
    {
        ProductId = productId;
        ProductName = productName ?? throw new ArgumentNullException(nameof(productName));
        UnitPrice = unitPrice > 0 ? unitPrice : throw new ArgumentException("Unit price must be positive");
        Quantity = quantity > 0 ? quantity : throw new ArgumentException("Quantity must be positive");
    }

    public void IncreaseQuantity(int quantity)
    {
        if (quantity <= 0)
            throw new ArgumentException("Quantity must be positive");
            
        Quantity += quantity;
    }

    public void DecreaseQuantity(int quantity)
    {
        if (quantity <= 0)
            throw new ArgumentException("Quantity must be positive");
            
        if (Quantity < quantity)
            throw new InvalidOperationException("Insufficient quantity");
            
        Quantity -= quantity;
    }
}
  

Domain Services

  
    public interface IOrderService
{
    Task<Order> CreateOrderAsync(int customerId, List<OrderItemDto> items);
    Task<bool> CanCustomerPlaceOrderAsync(int customerId);
}

public class OrderService : IOrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly ICustomerRepository _customerRepository;

    public OrderService(IOrderRepository orderRepository, ICustomerRepository customerRepository)
    {
        _orderRepository = orderRepository;
        _customerRepository = customerRepository;
    }

    public async Task<Order> CreateOrderAsync(int customerId, List<OrderItemDto> items)
    {
        var customer = await _customerRepository.GetByIdAsync(customerId);
        if (customer == null)
            throw new DomainException($"Customer with ID {customerId} not found");

        if (!await CanCustomerPlaceOrderAsync(customerId))
            throw new DomainException("Customer cannot place order at this time");

        var order = new Order(customerId);
        
        foreach (var item in items)
        {
            order.AddItem(item.ProductId, item.ProductName, item.UnitPrice, item.Quantity);
        }

        return order;
    }

    public async Task<bool> CanCustomerPlaceOrderAsync(int customerId)
    {
        // Business rules for order placement
        var recentOrders = await _orderRepository.GetRecentOrdersAsync(customerId, TimeSpan.FromHours(24));
        return recentOrders.Count < 10; // Limit orders per day
    }
}
  

Domain Events

  
    public class OrderCompletedEvent : IDomainEvent
{
    public int OrderId { get; }
    public int CustomerId { get; }
    public decimal TotalAmount { get; }
    public DateTime OccurredOn { get; }

    public OrderCompletedEvent(int orderId, int customerId, decimal totalAmount)
    {
        OrderId = orderId;
        CustomerId = customerId;
        TotalAmount = totalAmount;
        OccurredOn = DateTime.UtcNow;
    }
}

// Domain Event Handler
public class OrderCompletedEventHandler : INotificationHandler<OrderCompletedEvent>
{
    private readonly IEmailService _emailService;
    private readonly IInventoryService _inventoryService;

    public OrderCompletedEventHandler(IEmailService emailService, IInventoryService inventoryService)
    {
        _emailService = emailService;
        _inventoryService = inventoryService;
    }

    public async Task Handle(OrderCompletedEvent notification, CancellationToken cancellationToken)
    {
        // Send confirmation email
        await _emailService.SendOrderConfirmationAsync(notification.OrderId, notification.CustomerId);
        
        // Update inventory
        await _inventoryService.ReserveItemsForOrderAsync(notification.OrderId);
    }
}
  

5. Command Query Responsibility Segregation (CQRS)

CQRS Pattern Overview

CQRS separates read and write operations:

  • Commands : Write operations that change state

  • Queries : Read operations that return data without side effects

CQRS Implementation

Command Side

  
    // Commands
public static class OrderCommands
{
    public record CreateOrderCommand(int CustomerId, List<OrderItemDto> Items) : IRequest<int>;
    
    public record CancelOrderCommand(int OrderId) : IRequest;
    
    public record CompleteOrderCommand(int OrderId) : IRequest;
}

// Command Handlers
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, int>
{
    private readonly IOrderService _orderService;
    private readonly IOrderRepository _orderRepository;

    public CreateOrderCommandHandler(IOrderService orderService, IOrderRepository orderRepository)
    {
        _orderService = orderService;
        _orderRepository = orderRepository;
    }

    public async Task<int> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        var order = await _orderService.CreateOrderAsync(request.CustomerId, request.Items);
        
        await _orderRepository.AddAsync(order);
        
        return order.Id;
    }
}

public class CompleteOrderCommandHandler : IRequestHandler<CompleteOrderCommand>
{
    private readonly IOrderRepository _orderRepository;

    public CompleteOrderCommandHandler(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

    public async Task Handle(CompleteOrderCommand request, CancellationToken cancellationToken)
    {
        var order = await _orderRepository.GetByIdAsync(request.OrderId);
        if (order == null)
            throw new NotFoundException($"Order with ID {request.OrderId} not found");

        order.CompleteOrder();
        
        await _orderRepository.UpdateAsync(order);
    }
}
  

Query Side

  
    // Read Models
public class OrderDto
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public string CustomerName { get; set; } = string.Empty;
    public OrderStatus Status { get; set; }
    public decimal TotalAmount { get; set; }
    public DateTime CreatedAt { get; set; }
    public List<OrderItemDto> Items { get; set; } = new();
}

public class OrderItemDto
{
    public int ProductId { get; set; }
    public string ProductName { get; set; } = string.Empty;
    public decimal UnitPrice { get; set; }
    public int Quantity { get; set; }
    public decimal TotalPrice => UnitPrice * Quantity;
}

// Queries
public static class OrderQueries
{
    public record GetOrderQuery(int OrderId) : IRequest<OrderDto>;
    
    public record GetCustomerOrdersQuery(int CustomerId, int PageNumber = 1, int PageSize = 10) 
        : IRequest<PagedList<OrderDto>>;
    
    public record GetOrdersByStatusQuery(OrderStatus Status, int PageNumber = 1, int PageSize = 10) 
        : IRequest<PagedList<OrderDto>>;
}

// Query Handlers
public class GetOrderQueryHandler : IRequestHandler<GetOrderQuery, OrderDto>
{
    private readonly IOrderReadRepository _orderReadRepository;

    public GetOrderQueryHandler(IOrderReadRepository orderReadRepository)
    {
        _orderReadRepository = orderReadRepository;
    }

    public async Task<OrderDto> Handle(GetOrderQuery request, CancellationToken cancellationToken)
    {
        var order = await _orderReadRepository.GetOrderAsync(request.OrderId);
        if (order == null)
            throw new NotFoundException($"Order with ID {request.OrderId} not found");

        return order;
    }
}

public class GetCustomerOrdersQueryHandler : IRequestHandler<GetCustomerOrdersQuery, PagedList<OrderDto>>
{
    private readonly IOrderReadRepository _orderReadRepository;

    public GetCustomerOrdersQueryHandler(IOrderReadRepository orderReadRepository)
    {
        _orderReadRepository = orderReadRepository;
    }

    public async Task<PagedList<OrderDto>> Handle(
        GetCustomerOrdersQuery request, 
        CancellationToken cancellationToken)
    {
        return await _orderReadRepository.GetCustomerOrdersAsync(
            request.CustomerId, request.PageNumber, request.PageSize);
    }
}
  

Separate Read Database

  
    // Read Repository
public interface IOrderReadRepository
{
    Task<OrderDto?> GetOrderAsync(int orderId);
    Task<PagedList<OrderDto>> GetCustomerOrdersAsync(int customerId, int pageNumber, int pageSize);
    Task<PagedList<OrderDto>> GetOrdersByStatusAsync(OrderStatus status, int pageNumber, int pageSize);
}

public class OrderReadRepository : IOrderReadRepository
{
    private readonly ReadDbContext _context;

    public OrderReadRepository(ReadDbContext context)
    {
        _context = context;
    }

    public async Task<OrderDto?> GetOrderAsync(int orderId)
    {
        return await _context.Orders
            .Include(o => o.Items)
            .Where(o => o.Id == orderId)
            .Select(o => new OrderDto
            {
                Id = o.Id,
                CustomerId = o.CustomerId,
                CustomerName = o.Customer.Name,
                Status = o.Status,
                TotalAmount = o.TotalAmount,
                CreatedAt = o.CreatedAt,
                Items = o.Items.Select(i => new OrderItemDto
                {
                    ProductId = i.ProductId,
                    ProductName = i.ProductName,
                    UnitPrice = i.UnitPrice,
                    Quantity = i.Quantity
                }).ToList()
            })
            .FirstOrDefaultAsync();
    }

    public async Task<PagedList<OrderDto>> GetCustomerOrdersAsync(int customerId, int pageNumber, int pageSize)
    {
        var query = _context.Orders
            .Where(o => o.CustomerId == customerId)
            .OrderByDescending(o => o.CreatedAt)
            .Select(o => new OrderDto
            {
                Id = o.Id,
                CustomerId = o.CustomerId,
                CustomerName = o.Customer.Name,
                Status = o.Status,
                TotalAmount = o.TotalAmount,
                CreatedAt = o.CreatedAt
            });

        return await PagedList<OrderDto>.CreateAsync(query, pageNumber, pageSize);
    }
}
  

CQRS with Event Sourcing

  
    // Event Store
public interface IEventStore
{
    Task SaveEventsAsync(Guid aggregateId, IEnumerable<IDomainEvent> events, int expectedVersion);
    Task<List<IDomainEvent>> GetEventsAsync(Guid aggregateId);
    Task<List<IDomainEvent>> GetEventsAsync(Guid aggregateId, DateTime until);
}

// Event Sourced Aggregate
public abstract class EventSourcedAggregate : Entity
{
    private readonly List<IDomainEvent> _changes = new();

    public int Version { get; protected set; } = -1;

    public IReadOnlyCollection<IDomainEvent> GetUncommittedChanges() => _changes.AsReadOnly();

    public void MarkChangesAsCommitted() => _changes.Clear();

    protected void ApplyChange(IDomainEvent @event, bool isNew = true)
    {
        this.Apply(@event);
        
        if (isNew)
        {
            _changes.Add(@event);
        }
    }

    public void LoadFromHistory(IEnumerable<IDomainEvent> history)
    {
        foreach (var @event in history)
        {
            ApplyChange(@event, isNew: false);
            Version++;
        }
    }

    protected abstract void Apply(IDomainEvent @event);
}
  

6. Event Sourcing

Event Sourcing Implementation

  
    // Event-Sourced Order Aggregate
public class EventSourcedOrder : EventSourcedAggregate
{
    public int CustomerId { get; private set; }
    public OrderStatus Status { get; private set; }
    public decimal TotalAmount { get; private set; }
    private readonly List<OrderItem> _items = new();

    public EventSourcedOrder() { }

    public EventSourcedOrder(int customerId)
    {
        ApplyChange(new OrderCreatedEvent(customerId, Guid.NewGuid()));
    }

    public void AddItem(int productId, string productName, decimal unitPrice, int quantity)
    {
        ApplyChange(new OrderItemAddedEvent(Id, productId, productName, unitPrice, quantity));
    }

    public void CompleteOrder()
    {
        if (Status != OrderStatus.Pending)
            throw new DomainException("Only pending orders can be completed");

        ApplyChange(new OrderCompletedEvent(Id, CustomerId, TotalAmount));
    }

    protected override void Apply(IDomainEvent @event)
    {
        switch (@event)
        {
            case OrderCreatedEvent e:
                Apply(e);
                break;
            case OrderItemAddedEvent e:
                Apply(e);
                break;
            case OrderCompletedEvent e:
                Apply(e);
                break;
        }
    }

    private void Apply(OrderCreatedEvent @event)
    {
        Id = @event.OrderId;
        CustomerId = @event.CustomerId;
        Status = OrderStatus.Pending;
    }

    private void Apply(OrderItemAddedEvent @event)
    {
        var existingItem = _items.FirstOrDefault(i => i.ProductId == @event.ProductId);
        
        if (existingItem != null)
        {
            _items.Remove(existingItem);
            _items.Add(new OrderItem(
                @event.ProductId, 
                @event.ProductName, 
                @event.UnitPrice, 
                existingItem.Quantity + @event.Quantity));
        }
        else
        {
            _items.Add(new OrderItem(
                @event.ProductId, 
                @event.ProductName, 
                @event.UnitPrice, 
                @event.Quantity));
        }
        
        TotalAmount = _items.Sum(i => i.UnitPrice * i.Quantity);
    }

    private void Apply(OrderCompletedEvent @event)
    {
        Status = OrderStatus.Completed;
    }
}
  

Projections

  
    // Read Model Projections
public class OrderProjection
{
    private readonly ReadDbContext _context;

    public OrderProjection(ReadDbContext context)
    {
        _context = context;
    }

    public async Task Handle(OrderCreatedEvent @event)
    {
        var order = new OrderReadModel
        {
            Id = @event.OrderId,
            CustomerId = @event.CustomerId,
            Status = OrderStatus.Pending.ToString(),
            TotalAmount = 0,
            CreatedAt = @event.OccurredOn
        };

        _context.Orders.Add(order);
        await _context.SaveChangesAsync();
    }

    public async Task Handle(OrderItemAddedEvent @event)
    {
        var order = await _context.Orders.FindAsync(@event.OrderId);
        if (order != null)
        {
            order.TotalAmount += @event.UnitPrice * @event.Quantity;
            await _context.SaveChangesAsync();
        }
    }

    public async Task Handle(OrderCompletedEvent @event)
    {
        var order = await _context.Orders.FindAsync(@event.OrderId);
        if (order != null)
        {
            order.Status = OrderStatus.Completed.ToString();
            await _context.SaveChangesAsync();
        }
    }
}
  

7. Microservices Architecture

Microservices Structure

  
    ECommerceMicroservices/
├── ApiGateway/                 # API Gateway
├── OrderService/              # Order management
├── ProductService/            # Product catalog
├── CustomerService/           # Customer management
├── InventoryService/          # Inventory management
├── PaymentService/            # Payment processing
└── NotificationService/       # Email/SMS notifications
  

Order Service Implementation

  
    // Order Service - Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Database
builder.Services.AddDbContext<OrderDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("OrderDb")));

// MediatR
builder.Services.AddMediatR(cfg => 
    cfg.RegisterServicesFromAssembly(typeof(CreateOrderCommand).Assembly));

// Message Bus
builder.Services.AddRabbitMQ(builder.Configuration);

// Health Checks
builder.Services.AddHealthChecks()
    .AddSqlServer(builder.Configuration.GetConnectionString("OrderDb")!)
    .AddRabbitMQ();

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();

app.UseRouting();
app.UseAuthorization();

app.MapControllers();
app.MapHealthChecks("/health");

app.Run();
  

Inter-Service Communication

Synchronous HTTP Communication

  
    // Product Service Client
public interface IProductServiceClient
{
    Task<ProductDto?> GetProductAsync(int productId);
    Task<bool> ValidateProductsAsync(List<int> productIds);
}

public class ProductServiceClient : IProductServiceClient
{
    private readonly HttpClient _httpClient;
    private readonly ILogger<ProductServiceClient> _logger;

    public ProductServiceClient(HttpClient httpClient, ILogger<ProductServiceClient> logger)
    {
        _httpClient = httpClient;
        _logger = logger;
    }

    public async Task<ProductDto?> GetProductAsync(int productId)
    {
        try
        {
            var response = await _httpClient.GetAsync($"/api/products/{productId}");
            
            if (response.IsSuccessStatusCode)
            {
                return await response.Content.ReadFromJsonAsync<ProductDto>();
            }
            
            _logger.LogWarning("Failed to get product {ProductId}. Status: {StatusCode}", 
                productId, response.StatusCode);
            return null;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting product {ProductId}", productId);
            return null;
        }
    }
}

// HTTP Client Registration
builder.Services.AddHttpClient<IProductServiceClient, ProductServiceClient>(client =>
{
    client.BaseAddress = new Uri(builder.Configuration["Services:ProductService"]!);
    client.Timeout = TimeSpan.FromSeconds(30);
});
  

Asynchronous Message Communication

  
    // Event Publisher
public interface IEventPublisher
{
    Task PublishAsync<TEvent>(TEvent @event) where TEvent : class;
}

public class RabbitMQEventPublisher : IEventPublisher
{
    private readonly IConnection _connection;
    private readonly ILogger<RabbitMQEventPublisher> _logger;

    public RabbitMQEventPublisher(IConnection connection, ILogger<RabbitMQEventPublisher> logger)
    {
        _connection = connection;
        _logger = logger;
    }

    public async Task PublishAsync<TEvent>(TEvent @event) where TEvent : class
    {
        using var channel = _connection.CreateModel();
        
        var eventName = typeof(TEvent).Name;
        var message = JsonSerializer.Serialize(@event);
        var body = Encoding.UTF8.GetBytes(message);
        
        channel.BasicPublish(
            exchange: "ecommerce-events",
            routingKey: eventName,
            basicProperties: null,
            body: body);
            
        _logger.LogInformation("Published event: {EventName}", eventName);
    }
}

// Event Consumer
public class OrderCreatedEventConsumer : IHostedService
{
    private readonly IConnection _connection;
    private readonly ILogger<OrderCreatedEventConsumer> _logger;
    private readonly IServiceProvider _serviceProvider;
    private IModel? _channel;

    public OrderCreatedEventConsumer(
        IConnection connection,
        ILogger<OrderCreatedEventConsumer> logger,
        IServiceProvider serviceProvider)
    {
        _connection = connection;
        _logger = logger;
        _serviceProvider = serviceProvider;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _channel = _connection.CreateModel();
        
        _channel.ExchangeDeclare("ecommerce-events", ExchangeType.Topic, durable: true);
        _channel.QueueDeclare("notification-service", durable: true, exclusive: false, autoDelete: false);
        _channel.QueueBind("notification-service", "ecommerce-events", "OrderCreatedEvent");
        
        var consumer = new EventingBasicConsumer(_channel);
        consumer.Received += async (model, ea) =>
        {
            try
            {
                var body = ea.Body.ToArray();
                var message = Encoding.UTF8.GetString(body);
                var @event = JsonSerializer.Deserialize<OrderCreatedEvent>(message);
                
                if (@event != null)
                {
                    using var scope = _serviceProvider.CreateScope();
                    var handler = scope.ServiceProvider.GetRequiredService<INotificationHandler<OrderCreatedEvent>>();
                    await handler.Handle(@event, CancellationToken.None);
                }
                
                _channel.BasicAck(ea.DeliveryTag, false);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error processing OrderCreatedEvent");
                _channel.BasicNack(ea.DeliveryTag, false, true);
            }
        };
        
        _channel.BasicConsume("notification-service", false, consumer);
        
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _channel?.Close();
        return Task.CompletedTask;
    }
}
  

API Gateway

  
    // Ocelot Configuration
{
  "Routes": [
    {
      "DownstreamPathTemplate": "/api/orders",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "orderservice",
          "Port": 443
        }
      ],
      "UpstreamPathTemplate": "/orders",
      "UpstreamHttpMethod": [ "GET", "POST" ]
    },
    {
      "DownstreamPathTemplate": "/api/products",
      "DownstreamScheme": "https",
      "DownstreamHostAndPorts": [
        {
          "Host": "productservice",
          "Port": 443
        }
      ],
      "UpstreamPathTemplate": "/products",
      "UpstreamHttpMethod": [ "GET", "POST" ]
    }
  ],
  "GlobalConfiguration": {
    "BaseUrl": "https://api-gateway:443"
  }
}
  

8. Vertical Slice Architecture

Vertical Slice Structure

  
    ECommerceVertical/
├── Features/
│   ├── Products/
│   │   ├── CreateProduct/
│   │   ├── GetProduct/
│   │   ├── UpdateProduct/
│   │   └── DeleteProduct/
│   ├── Orders/
│   │   ├── CreateOrder/
│   │   ├── GetOrder/
│   │   └── CancelOrder/
│   └── Customers/
│       ├── RegisterCustomer/
│       └── GetCustomerProfile/
├── Shared/
│   ├── Domain/
│   ├── Infrastructure/
│   └── Common/
└── ECommerceVertical.Web/
  

Feature Implementation

  
    // Features/Products/CreateProduct/
public record CreateProductCommand(string Name, string Description, decimal Price, int StockQuantity) 
    : IRequest<int>;

public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, int>
{
    private readonly ApplicationDbContext _context;

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

    public async Task<int> Handle(CreateProductCommand request, CancellationToken cancellationToken)
    {
        var product = new Product
        {
            Name = request.Name,
            Description = request.Description,
            Price = request.Price,
            StockQuantity = request.StockQuantity,
            CreatedAt = DateTime.UtcNow
        };

        _context.Products.Add(product);
        await _context.SaveChangesAsync(cancellationToken);

        return product.Id;
    }
}

public class CreateProductEndpoint : ICarterModule
{
    public void AddRoutes(IEndpointRouteBuilder app)
    {
        app.MapPost("/products", async (CreateProductCommand command, IMediator mediator) =>
        {
            var productId = await mediator.Send(command);
            return Results.Created($"/products/{productId}", new { productId });
        })
        .WithName("CreateProduct")
        .Produces<int>(StatusCodes.Status201Created)
        .Produces(StatusCodes.Status400BadRequest);
    }
}
  

9. Repository Pattern & Unit of Work

Generic Repository

  
    public interface IRepository<T> where T : class
{
    Task<T?> GetByIdAsync(int id);
    Task<IReadOnlyList<T>> GetAllAsync();
    Task<IReadOnlyList<T>> GetAsync(Expression<Func<T, bool>> predicate);
    Task<T> AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(T entity);
}

public class Repository<T> : IRepository<T> where T : class
{
    protected readonly ApplicationDbContext _context;

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

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

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

    public virtual async Task<IReadOnlyList<T>> GetAsync(Expression<Func<T, bool>> predicate)
    {
        return await _context.Set<T>().Where(predicate).ToListAsync();
    }

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

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

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

Specific Repository

  
    public interface IProductRepository : IRepository<Product>
{
    Task<IReadOnlyList<Product>> GetProductsByCategoryAsync(int categoryId);
    Task<IReadOnlyList<Product>> GetOutOfStockProductsAsync();
    Task<Product?> GetProductWithCategoryAsync(int productId);
}

public class ProductRepository : Repository<Product>, IProductRepository
{
    public ProductRepository(ApplicationDbContext context) : base(context)
    {
    }

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

    public async Task<IReadOnlyList<Product>> GetOutOfStockProductsAsync()
    {
        return await _context.Products
            .Where(p => p.StockQuantity == 0)
            .ToListAsync();
    }

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

Unit of Work Pattern

  
    public interface IUnitOfWork : IDisposable
{
    IProductRepository Products { get; }
    IOrderRepository Orders { get; }
    ICustomerRepository Customers { get; }
    
    Task<int> SaveChangesAsync();
    Task BeginTransactionAsync();
    Task CommitTransactionAsync();
    Task RollbackTransactionAsync();
}

public class UnitOfWork : IUnitOfWork
{
    private readonly ApplicationDbContext _context;
    private IDbContextTransaction? _transaction;

    public UnitOfWork(ApplicationDbContext context)
    {
        _context = context;
        Products = new ProductRepository(_context);
        Orders = new OrderRepository(_context);
        Customers = new CustomerRepository(_context);
    }

    public IProductRepository Products { get; }
    public IOrderRepository Orders { get; }
    public ICustomerRepository Customers { get; }

    public async Task<int> SaveChangesAsync()
    {
        return await _context.SaveChangesAsync();
    }

    public async Task BeginTransactionAsync()
    {
        _transaction = await _context.Database.BeginTransactionAsync();
    }

    public async Task CommitTransactionAsync()
    {
        if (_transaction != null)
        {
            await _transaction.CommitAsync();
            await _transaction.DisposeAsync();
            _transaction = null;
        }
    }

    public async Task RollbackTransactionAsync()
    {
        if (_transaction != null)
        {
            await _transaction.RollbackAsync();
            await _transaction.DisposeAsync();
            _transaction = null;
        }
    }

    public void Dispose()
    {
        _transaction?.Dispose();
        _context.Dispose();
    }
}
  

10. Real-World E-Commerce Implementation

Complete E-Commerce Architecture

  
    // Domain Models
public class ShoppingCart : Entity, IAggregateRoot
{
    private readonly List<CartItem> _items = new();
    
    public int CustomerId { get; private set; }
    public DateTime CreatedAt { get; private set; }
    public DateTime UpdatedAt { get; private set; }
    public IReadOnlyCollection<CartItem> Items => _items.AsReadOnly();
    public decimal TotalPrice => _items.Sum(item => item.UnitPrice * item.Quantity);

    private ShoppingCart() { }

    public ShoppingCart(int customerId)
    {
        CustomerId = customerId;
        CreatedAt = DateTime.UtcNow;
        UpdatedAt = DateTime.UtcNow;
    }

    public void AddItem(int productId, string productName, decimal unitPrice, int quantity = 1)
    {
        var existingItem = _items.FirstOrDefault(item => item.ProductId == productId);
        
        if (existingItem != null)
        {
            existingItem.IncreaseQuantity(quantity);
        }
        else
        {
            var cartItem = new CartItem(productId, productName, unitPrice, quantity);
            _items.Add(cartItem);
        }
        
        UpdatedAt = DateTime.UtcNow;
    }

    public void RemoveItem(int productId)
    {
        var item = _items.FirstOrDefault(item => item.ProductId == productId);
        if (item != null)
        {
            _items.Remove(item);
            UpdatedAt = DateTime.UtcNow;
        }
    }

    public void UpdateItemQuantity(int productId, int quantity)
    {
        var item = _items.FirstOrDefault(item => item.ProductId == productId);
        if (item != null)
        {
            if (quantity <= 0)
            {
                _items.Remove(item);
            }
            else
            {
                item.UpdateQuantity(quantity);
            }
            
            UpdatedAt = DateTime.UtcNow;
        }
    }

    public void Clear()
    {
        _items.Clear();
        UpdatedAt = DateTime.UtcNow;
    }

    public Order Checkout()
    {
        if (!_items.Any())
            throw new DomainException("Cannot checkout empty cart");
            
        var order = new Order(CustomerId);
        
        foreach (var item in _items)
        {
            order.AddItem(item.ProductId, item.ProductName, item.UnitPrice, item.Quantity);
        }
        
        Clear();
        
        return order;
    }
}

public class CartItem : Entity
{
    public int ProductId { get; private set; }
    public string ProductName { get; private set; } = string.Empty;
    public decimal UnitPrice { get; private set; }
    public int Quantity { get; private set; }

    private CartItem() { }

    public CartItem(int productId, string productName, decimal unitPrice, int quantity)
    {
        ProductId = productId;
        ProductName = productName ?? throw new ArgumentNullException(nameof(productName));
        UnitPrice = unitPrice > 0 ? unitPrice : throw new ArgumentException("Unit price must be positive");
        Quantity = quantity > 0 ? quantity : throw new ArgumentException("Quantity must be positive");
    }

    public void IncreaseQuantity(int quantity = 1)
    {
        if (quantity <= 0)
            throw new ArgumentException("Quantity must be positive");
            
        Quantity += quantity;
    }

    public void UpdateQuantity(int quantity)
    {
        if (quantity <= 0)
            throw new ArgumentException("Quantity must be positive");
            
        Quantity = quantity;
    }
}
  

Application Services

  
    // Shopping Cart Service
public interface IShoppingCartService
{
    Task<ShoppingCart> GetCartAsync(int customerId);
    Task AddItemToCartAsync(int customerId, int productId, string productName, decimal unitPrice, int quantity);
    Task RemoveItemFromCartAsync(int customerId, int productId);
    Task UpdateCartItemQuantityAsync(int customerId, int productId, int quantity);
    Task<Order> CheckoutAsync(int customerId);
}

public class ShoppingCartService : IShoppingCartService
{
    private readonly IShoppingCartRepository _cartRepository;
    private readonly IProductRepository _productRepository;
    private readonly IOrderService _orderService;

    public ShoppingCartService(
        IShoppingCartRepository cartRepository,
        IProductRepository productRepository,
        IOrderService orderService)
    {
        _cartRepository = cartRepository;
        _productRepository = productRepository;
        _orderService = orderService;
    }

    public async Task<ShoppingCart> GetCartAsync(int customerId)
    {
        var cart = await _cartRepository.GetByCustomerIdAsync(customerId);
        
        if (cart == null)
        {
            cart = new ShoppingCart(customerId);
            await _cartRepository.AddAsync(cart);
        }
        
        return cart;
    }

    public async Task AddItemToCartAsync(int customerId, int productId, string productName, decimal unitPrice, int quantity)
    {
        var cart = await GetCartAsync(customerId);
        
        // Validate product exists and has sufficient stock
        var product = await _productRepository.GetByIdAsync(productId);
        if (product == null)
            throw new DomainException($"Product with ID {productId} not found");
            
        if (product.StockQuantity < quantity)
            throw new DomainException($"Insufficient stock for product {product.Name}");

        cart.AddItem(productId, productName, unitPrice, quantity);
        
        await _cartRepository.UpdateAsync(cart);
    }

    public async Task<Order> CheckoutAsync(int customerId)
    {
        var cart = await GetCartAsync(customerId);
        
        if (!cart.Items.Any())
            throw new DomainException("Cannot checkout empty cart");
            
        // Validate all items are still available
        foreach (var item in cart.Items)
        {
            var product = await _productRepository.GetByIdAsync(item.ProductId);
            if (product == null || product.StockQuantity < item.Quantity)
            {
                throw new DomainException($"Product {item.ProductName} is no longer available in requested quantity");
            }
        }
        
        var order = cart.Checkout();
        
        await _orderService.CreateOrderAsync(order);
        await _cartRepository.UpdateAsync(cart);
        
        return order;
    }
}
  

API Controllers

  
    [ApiController]
[Route("api/[controller]")]
public class ShoppingCartController : ControllerBase
{
    private readonly IShoppingCartService _shoppingCartService;
    private readonly IMediator _mediator;

    public ShoppingCartController(IShoppingCartService shoppingCartService, IMediator mediator)
    {
        _shoppingCartService = shoppingCartService;
        _mediator = mediator;
    }

    [HttpGet]
    public async Task<ActionResult<ShoppingCartDto>> GetCart()
    {
        var customerId = GetCustomerId(); // From auth
        var cart = await _shoppingCartService.GetCartAsync(customerId);
        
        return Ok(MapToDto(cart));
    }

    [HttpPost("items")]
    public async Task<ActionResult> AddItem(AddCartItemRequest request)
    {
        try
        {
            var customerId = GetCustomerId();
            await _shoppingCartService.AddItemToCartAsync(
                customerId, request.ProductId, request.ProductName, request.UnitPrice, request.Quantity);
                
            return Ok();
        }
        catch (DomainException ex)
        {
            return BadRequest(ex.Message);
        }
    }

    [HttpPost("checkout")]
    public async Task<ActionResult<OrderDto>> Checkout()
    {
        try
        {
            var customerId = GetCustomerId();
            var order = await _shoppingCartService.CheckoutAsync(customerId);
            
            var orderDto = await _mediator.Send(new GetOrderQuery(order.Id));
            
            return Ok(orderDto);
        }
        catch (DomainException ex)
        {
            return BadRequest(ex.Message);
        }
    }

    private int GetCustomerId()
    {
        // Extract from JWT token or other auth mechanism
        return int.Parse(User.FindFirst("customerId")?.Value ?? "0");
    }

    private ShoppingCartDto MapToDto(ShoppingCart cart)
    {
        return new ShoppingCartDto
        {
            CustomerId = cart.CustomerId,
            Items = cart.Items.Select(item => new CartItemDto
            {
                ProductId = item.ProductId,
                ProductName = item.ProductName,
                UnitPrice = item.UnitPrice,
                Quantity = item.Quantity,
                TotalPrice = item.UnitPrice * item.Quantity
            }).ToList(),
            TotalPrice = cart.TotalPrice,
            ItemCount = cart.Items.Count
        };
    }
}
  

11. Testing Strategies

Unit Tests

  
    // Domain Unit Tests
public class ShoppingCartTests
{
    [Fact]
    public void AddItem_ShouldAddNewItemToCart()
    {
        // Arrange
        var cart = new ShoppingCart(1);
        var productId = 1;
        var productName = "Test Product";
        var unitPrice = 10.0m;
        var quantity = 2;

        // Act
        cart.AddItem(productId, productName, unitPrice, quantity);

        // Assert
        cart.Items.Should().HaveCount(1);
        cart.Items.First().ProductId.Should().Be(productId);
        cart.Items.First().Quantity.Should().Be(quantity);
        cart.TotalPrice.Should().Be(20.0m);
    }

    [Fact]
    public void AddItem_ExistingProduct_ShouldIncreaseQuantity()
    {
        // Arrange
        var cart = new ShoppingCart(1);
        cart.AddItem(1, "Product", 10.0m, 2);

        // Act
        cart.AddItem(1, "Product", 10.0m, 3);

        // Assert
        cart.Items.Should().HaveCount(1);
        cart.Items.First().Quantity.Should().Be(5);
        cart.TotalPrice.Should().Be(50.0m);
    }

    [Fact]
    public void Checkout_EmptyCart_ShouldThrowException()
    {
        // Arrange
        var cart = new ShoppingCart(1);

        // Act & Assert
        cart.Invoking(c => c.Checkout())
            .Should().Throw<DomainException>()
            .WithMessage("Cannot checkout empty cart");
    }
}

// Command Handler Tests
public class CreateProductCommandHandlerTests
{
    [Fact]
    public async Task Handle_ValidCommand_ShouldCreateProduct()
    {
        // Arrange
        var context = new Mock<ApplicationDbContext>();
        var products = new List<Product>();
        var mockSet = products.AsQueryable().BuildMockDbSet();
        
        context.Setup(c => c.Products).Returns(mockSet.Object);
        context.Setup(c => c.SaveChangesAsync(It.IsAny<CancellationToken>()))
               .ReturnsAsync(1);
        
        var handler = new CreateProductCommandHandler(context.Object);
        var command = new CreateProductCommand("Test Product", "Description", 10.0m, 100);

        // Act
        var result = await handler.Handle(command, CancellationToken.None);

        // Assert
        result.Should().BeGreaterThan(0);
        products.Should().HaveCount(1);
        products.First().Name.Should().Be("Test Product");
    }
}
  

Integration Tests

  
    public class ProductApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    private readonly HttpClient _client;

    public ProductApiIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task CreateProduct_ValidRequest_ShouldReturnCreated()
    {
        // Arrange
        var request = new CreateProductRequest
        {
            Name = "Integration Test Product",
            Description = "Test Description",
            Price = 15.99m,
            StockQuantity = 50
        };

        // Act
        var response = await _client.PostAsJsonAsync("/api/products", request);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.Created);
        response.Headers.Location.Should().NotBeNull();
    }

    [Fact]
    public async Task GetProduct_NonExistingId_ShouldReturnNotFound()
    {
        // Act
        var response = await _client.GetAsync("/api/products/9999");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.NotFound);
    }
}
  

12. Best Practices & Anti-Patterns

Architecture Best Practices

Do

  • Choose architecture based on project complexity and team size

  • Keep domain logic in domain layer

  • Use aggregates to enforce consistency boundaries

  • Implement proper exception handling strategies

  • Use asynchronous programming appropriately

  • Implement comprehensive logging and monitoring

  • Design for testability from the beginning

Don't

  • Over-engineer simple applications

  • Mix concerns between layers

  • Create god classes or services

  • Ignore performance implications

  • Forget about security considerations

  • Neglect error handling and resilience

Common Anti-Patterns

  
    // ANTI-PATTERN: Anemic Domain Model
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    // No behavior - all logic in services
}

// BETTER: Rich Domain Model
public class Product : Entity
{
    public string Name { get; private set; }
    public decimal Price { get; private set; }

    public void UpdatePrice(decimal newPrice)
    {
        if (newPrice <= 0)
            throw new DomainException("Price must be positive");
        
        Price = newPrice;
    }
}

// ANTI-PATTERN: Service with too many responsibilities
public class ProductService
{
    public void CreateProduct() { /* ... */ }
    public void UpdateProduct() { /* ... */ }
    public void DeleteProduct() { /* ... */ }
    public void ProcessOrder() { /* ... */ } // Wrong responsibility!
    public void SendEmail() { /* ... */ } // Wrong responsibility!
}

// BETTER: Single responsibility services
public class ProductService { /* Product operations only */ }
public class OrderService { /* Order operations only */ }
public class EmailService { /* Email operations only */ }
  

Migration Strategy

  
    // Gradual migration from legacy to modern architecture
public class LegacyToCleanMigration
{
    // Step 1: Extract domain logic from services
    // Step 2: Implement repository pattern
    // Step 3: Introduce commands and queries
    // Step 4: Refactor to vertical slices
    // Step 5: Implement CQRS and event sourcing
}
  

This comprehensive guide covers enterprise-level  ASP.NET  Core architecture patterns with real-world implementations, providing you with the knowledge to build scalable, maintainable, and resilient systems using industry-best practices.