![image]()
Previous article: ASP.NET Core Design Patterns Revolution: Repository, Singleton & Clean Architecture (Part-18 of 40)
Table of Contents
Introduction to Architectural Patterns
Layered Architecture
Clean Architecture & Onion Architecture
Domain-Driven Design (DDD)
Command Query Responsibility Segregation (CQRS)
Event Sourcing
Microservices Architecture
Vertical Slice Architecture
Repository Pattern & Unit of Work
Real-World E-Commerce Implementation
Testing Strategies
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:
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.