实现完整 CQRS + MediatR 订单系统
📋 项目概述
本实战案例将演示如何从零开始构建一个基于 CQRS(命令查询职责分离) 和 MediatR 的完整订单管理系统。该系统包含订单创建、修改、查询、取消等核心功能,并采用分层架构设计。
学习目标
- ✅ 理解 CQRS 模式在实际项目中的应用
- ✅ 掌握 MediatR 在复杂业务场景中的使用
- ✅ 学习领域层、应用层、基础设施层的分离
- ✅ 实践事件驱动架构
- ✅ 掌握命令验证、管道行为、错误处理
系统架构图
mermaid
graph TB
subgraph "表现层"
API[ASP.NET Core API]
Swagger[Swagger UI]
end
subgraph "应用层 - Commands"
CreateOrderCmd[CreateOrderCommand]
UpdateOrderCmd[UpdateOrderCommand]
CancelOrderCmd[CancelOrderCommand]
end
subgraph "应用层 - Queries"
GetOrderQuery[GetOrderQuery]
ListOrdersQuery[ListOrdersQuery]
end
subgraph "应用层 - Handlers"
CreateHandler[CreateOrderHandler]
UpdateHandler[UpdateOrderHandler]
CancelHandler[CancelOrderHandler]
GetHandler[GetOrderHandler]
ListHandler[ListOrdersHandler]
end
subgraph "领域层"
Order[Order Aggregate Root]
OrderItem[Order Item Entity]
Events[Domain Events]
end
subgraph "基础设施层"
DbContext[EF Core DbContext]
Repositories[Repositories]
EventBus[Event Bus]
end
API --> CreateOrderCmd
API --> GetOrderQuery
CreateOrderCmd --> CreateHandler
GetOrderQuery --> GetHandler
CreateHandler --> Order
GetHandler --> Repositories
Order --> Events
Events --> EventBus🏗️ 项目结构设计
解决方案结构
OrderManagementSystem/
├── src/
│ ├── OrderManagement.Domain/ # 领域层
│ │ ├── AggregatesModel/
│ │ │ ├── OrderAggregate/
│ │ │ │ ├── Order.cs
│ │ │ │ ├── OrderItem.cs
│ │ │ │ ├── OrderStatus.cs
│ │ │ │ └── Address.cs
│ │ │ └── CustomerAggregate/
│ │ │ └── Customer.cs
│ │ ├── Events/
│ │ │ ├── OrderCreatedEvent.cs
│ │ │ ├── OrderCancelledEvent.cs
│ │ │ └── OrderUpdatedEvent.cs
│ │ ├── Exceptions/
│ │ │ └── DomainException.cs
│ │ └── ValueObjects/
│ │ └── Money.cs
│ │
│ ├── OrderManagement.Application/ # 应用层
│ │ ├── Commands/
│ │ │ ├── Orders/
│ │ │ │ ├── CreateOrder/
│ │ │ │ │ ├── CreateOrderCommand.cs
│ │ │ │ │ ├── CreateOrderValidator.cs
│ │ │ │ │ └── CreateOrderHandler.cs
│ │ │ │ ├── UpdateOrder/
│ │ │ │ ├── CancelOrder/
│ │ │ │ └── ShipOrder/
│ │ │ └── Customers/
│ │ │
│ │ ├── Queries/
│ │ │ ├── Orders/
│ │ │ │ ├── GetOrder/
│ │ │ │ │ ├── GetOrderQuery.cs
│ │ │ │ │ ├── GetOrderHandler.cs
│ │ │ │ │ └── OrderDto.cs
│ │ │ │ └── ListOrders/
│ │ │ └── Customers/
│ │ │
│ │ ├── Behaviors/
│ │ │ ├── ValidationBehavior.cs
│ │ │ ├── LoggingBehavior.cs
│ │ │ └── TransactionBehavior.cs
│ │ │
│ │ ├── Interfaces/
│ │ │ ├── IOrderRepository.cs
│ │ │ ├── ICustomerRepository.cs
│ │ │ └── IUnitOfWork.cs
│ │ │
│ │ └── DependencyInjection.cs
│ │
│ ├── OrderManagement.Infrastructure/ # 基础设施层
│ │ ├── Persistence/
│ │ │ ├── ApplicationDbContext.cs
│ │ │ ├── Configurations/
│ │ │ │ ├── OrderConfiguration.cs
│ │ │ │ └── CustomerConfiguration.cs
│ │ │ └── Migrations/
│ │ │
│ │ ├── Repositories/
│ │ │ ├── OrderRepository.cs
│ │ │ └── CustomerRepository.cs
│ │ │
│ │ ├── EventHandlers/
│ │ │ ├── OrderCreatedEventHandler.cs
│ │ │ └── OrderCancelledEventHandler.cs
│ │ │
│ │ └── DependencyInjection.cs
│ │
│ └── OrderManagement.API/ # API 层
│ ├── Controllers/
│ │ ├── OrdersController.cs
│ │ └── CustomersController.cs
│ ├── Program.cs
│ └── appsettings.json
│
└── tests/
├── OrderManagement.UnitTests/ # 单元测试
└── OrderManagement.IntegrationTests/ # 集成测试🎯 领域层实现
1. 值对象 - Money
csharp
// OrderManagement.Domain/ValueObjects/Money.cs
namespace OrderManagement.Domain.ValueObjects;
public class Money : IEquatable<Money>
{
public decimal Amount { get; private set; }
public string Currency { get; private set; }
private Money(decimal amount, string currency)
{
if (amount < 0)
throw new ArgumentException("金额不能为负数", nameof(amount));
Amount = amount;
Currency = currency ?? throw new ArgumentNullException(nameof(currency));
}
public static Money FromDecimal(decimal amount, string currency = "CNY")
{
return new Money(amount, currency);
}
public static Money operator +(Money a, Money b)
{
if (a.Currency != b.Currency)
throw new InvalidOperationException("货币类型不一致");
return new Money(a.Amount + b.Amount, a.Currency);
}
public bool Equals(Money? other)
{
if (other is null) return false;
return Amount == other.Amount && Currency == other.Currency;
}
public override bool Equals(object? obj) => Equals(obj as Money);
public override int GetHashCode() => HashCode.Combine(Amount, Currency);
}2. 枚举 - 订单状态
csharp
// OrderManagement.Domain/AggregatesModel/OrderAggregate/OrderStatus.cs
namespace OrderManagement.Domain.AggregatesModel.OrderAggregate;
public enum OrderStatus
{
Pending = 1, // 待处理
Confirmed = 2, // 已确认
Shipped = 3, // 已发货
Delivered = 4, // 已送达
Cancelled = 5, // 已取消
Refunded = 6 // 已退款
}3. 聚合根 - Order
csharp
// OrderManagement.Domain/AggregatesModel/OrderAggregate/Order.cs
using OrderManagement.Domain.Events;
using OrderManagement.Domain.ValueObjects;
namespace OrderManagement.Domain.AggregatesModel.OrderAggregate;
public class Order : IAggregateRoot
{
public int Id { get; private set; }
public Guid OrderNumber { get; private set; }
public int CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
private readonly List<OrderItem> _orderItems = new();
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();
public Money TotalAmount { get; private set; }
public Address ShippingAddress { get; private set; }
public DateTime CreatedAt { get; private set; }
public DateTime? UpdatedAt { get; private set; }
public string? Remark { get; private set; }
// EF Core 需要无参构造函数
private Order()
{
OrderNumber = Guid.NewGuid();
Status = OrderStatus.Pending;
CreatedAt = DateTime.UtcNow;
TotalAmount = Money.FromDecimal(0);
}
public Order(int customerId, Address shippingAddress, IEnumerable<OrderItem> items, string? remark = null)
{
CustomerId = customerId;
ShippingAddress = shippingAddress ?? throw new ArgumentNullException(nameof(shippingAddress));
Remark = remark;
OrderNumber = Guid.NewGuid();
Status = OrderStatus.Pending;
CreatedAt = DateTime.UtcNow;
_orderItems.AddRange(items);
RecalculateTotalAmount();
// 触发领域事件
AddDomainEvent(new OrderCreatedEvent(Id, OrderNumber, CustomerId, CreatedAt));
}
public void Confirm()
{
if (Status != OrderStatus.Pending)
throw new DomainException($"订单状态为 {Status},无法确认");
Status = OrderStatus.Confirmed;
UpdatedAt = DateTime.UtcNow;
AddDomainEvent(new OrderUpdatedEvent(Id, OrderNumber, Status));
}
public void Ship()
{
if (Status != OrderStatus.Confirmed)
throw new DomainException($"订单状态为 {Status},无法发货");
Status = OrderStatus.Shipped;
UpdatedAt = DateTime.UtcNow;
}
public void Deliver()
{
if (Status != OrderStatus.Shipped)
throw new DomainException($"订单状态为 {Status},无法送达");
Status = OrderStatus.Delivered;
UpdatedAt = DateTime.UtcNow;
}
public void Cancel(string reason)
{
if (Status == OrderStatus.Cancelled)
throw new DomainException("订单已取消");
if (Status == OrderStatus.Delivered)
throw new DomainException("订单已送达,无法取消");
Status = OrderStatus.Cancelled;
UpdatedAt = DateTime.UtcNow;
Remark = $"{Remark}\n取消原因: {reason}";
AddDomainEvent(new OrderCancelledEvent(Id, OrderNumber, reason));
}
public void UpdateShippingAddress(Address newAddress)
{
if (Status != OrderStatus.Pending && Status != OrderStatus.Confirmed)
throw new DomainException($"订单状态为 {Status},无法修改收货地址");
ShippingAddress = newAddress ?? throw new ArgumentNullException(nameof(newAddress));
UpdatedAt = DateTime.UtcNow;
}
public void AddOrderItem(OrderItem item)
{
if (Status != OrderStatus.Pending)
throw new DomainException($"订单状态为 {Status},无法添加商品");
_orderItems.Add(item);
RecalculateTotalAmount();
}
public void RemoveOrderItem(int productId)
{
if (Status != OrderStatus.Pending)
throw new DomainException($"订单状态为 {Status},无法删除商品");
var item = _orderItems.FirstOrDefault(x => x.ProductId == productId);
if (item != null)
{
_orderItems.Remove(item);
RecalculateTotalAmount();
}
}
private void RecalculateTotalAmount()
{
var total = _orderItems.Sum(x => x.TotalPrice.Amount);
TotalAmount = Money.FromDecimal(total, "CNY");
}
}4. 实体 - OrderItem
csharp
// OrderManagement.Domain/AggregatesModel/OrderAggregate/OrderItem.cs
using OrderManagement.Domain.ValueObjects;
namespace OrderManagement.Domain.AggregatesModel.OrderAggregate;
public class OrderItem
{
public int Id { get; private set; }
public int OrderId { get; private set; }
public int ProductId { get; private set; }
public string ProductName { get; private set; }
public int Quantity { get; private set; }
public Money UnitPrice { get; private set; }
public Money TotalPrice { get; private set; }
private OrderItem() { }
public OrderItem(int productId, string productName, int quantity, Money unitPrice)
{
if (quantity <= 0)
throw new DomainException("商品数量必须大于0");
ProductId = productId;
ProductName = productName ?? throw new ArgumentNullException(nameof(productName));
Quantity = quantity;
UnitPrice = unitPrice ?? throw new ArgumentNullException(nameof(unitPrice));
TotalPrice = Money.FromDecimal(unitPrice.Amount * quantity, unitPrice.Currency);
}
}5. 值对象 - Address
csharp
// OrderManagement.Domain/AggregatesModel/OrderAggregate/Address.cs
namespace OrderManagement.Domain.AggregatesModel.OrderAggregate;
public class Address : IEquatable<Address>
{
public string Province { get; private set; }
public string City { get; private set; }
public string District { get; private set; }
public string Street { get; private set; }
public string ZipCode { get; private set; }
private Address() { }
public Address(string province, string city, string district, string street, string zipCode)
{
Province = province ?? throw new ArgumentNullException(nameof(province));
City = city ?? throw new ArgumentNullException(nameof(city));
District = district ?? throw new ArgumentNullException(nameof(district));
Street = street ?? throw new ArgumentNullException(nameof(street));
ZipCode = zipCode ?? throw new ArgumentNullException(nameof(zipCode));
}
public override string ToString()
{
return $"{Province}{City}{District}{Street} {ZipCode}";
}
public bool Equals(Address? other)
{
if (other is null) return false;
return Province == other.Province &&
City == other.City &&
District == other.District &&
Street == other.Street &&
ZipCode == other.ZipCode;
}
public override bool Equals(object? obj) => Equals(obj as Address);
public override int GetHashCode() => HashCode.Combine(Province, City, District, Street, ZipCode);
}6. 领域事件
csharp
// OrderManagement.Domain/Events/OrderCreatedEvent.cs
using MediatR;
namespace OrderManagement.Domain.Events;
public record OrderCreatedEvent(int OrderId, Guid OrderNumber, int CustomerId, DateTime CreatedAt)
: INotification
{
public string EventType => nameof(OrderCreatedEvent);
}
// OrderManagement.Domain/Events/OrderCancelledEvent.cs
public record OrderCancelledEvent(int OrderId, Guid OrderNumber, string Reason)
: INotification
{
public string EventType => nameof(OrderCancelledEvent);
}
// OrderManagement.Domain/Events/OrderUpdatedEvent.cs
public record OrderUpdatedEvent(int OrderId, Guid OrderNumber, OrderStatus NewStatus)
: INotification
{
public string EventType => nameof(OrderUpdatedEvent);
}🚀 应用层实现
1. 创建订单命令
csharp
// OrderManagement.Application/Commands/Orders/CreateOrder/CreateOrderCommand.cs
using MediatR;
using OrderManagement.Domain.AggregatesModel.OrderAggregate;
namespace OrderManagement.Application.Commands.Orders.CreateOrder;
public record CreateOrderCommand : IRequest<int>
{
public int CustomerId { get; init; }
public AddressDto ShippingAddress { get; init; }
public List<OrderItemDto> Items { get; init; } = new();
public string? Remark { get; init; }
}
public record AddressDto
{
public string Province { get; init; }
public string City { get; init; }
public string District { get; init; }
public string Street { get; init; }
public string ZipCode { get; init; }
public Address ToDomain()
{
return new Address(Province, City, District, Street, ZipCode);
}
}
public record OrderItemDto
{
public int ProductId { get; init; }
public string ProductName { get; init; }
public int Quantity { get; init; }
public decimal UnitPrice { get; init; }
public string Currency { get; init; } = "CNY";
}2. 命令验证器
csharp
// OrderManagement.Application/Commands/Orders/CreateOrder/CreateOrderValidator.cs
using FluentValidation;
namespace OrderManagement.Application.Commands.Orders.CreateOrder;
public class CreateOrderValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderValidator()
{
RuleFor(x => x.CustomerId)
.GreaterThan(0)
.WithMessage("客户ID无效");
RuleFor(x => x.ShippingAddress)
.NotNull()
.WithMessage("收货地址不能为空");
When(x => x.ShippingAddress != null, () =>
{
RuleFor(x => x.ShippingAddress.Province)
.NotEmpty().WithMessage("省份不能为空");
RuleFor(x => x.ShippingAddress.City)
.NotEmpty().WithMessage("城市不能为空");
RuleFor(x => x.ShippingAddress.Street)
.NotEmpty().WithMessage("街道不能为空");
});
RuleFor(x => x.Items)
.NotEmpty().WithMessage("订单商品不能为空")
.Must(x => x.Count > 0).WithMessage("至少需要一个商品");
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(x => x.ProductId)
.GreaterThan(0).WithMessage("商品ID无效");
item.RuleFor(x => x.ProductName)
.NotEmpty().WithMessage("商品名称不能为空");
item.RuleFor(x => x.Quantity)
.GreaterThan(0).WithMessage("商品数量必须大于0");
item.RuleFor(x => x.UnitPrice)
.GreaterThanOrEqualTo(0).WithMessage("商品单价不能为负数");
});
}
}3. 创建订单处理器
csharp
// OrderManagement.Application/Commands/Orders/CreateOrder/CreateOrderHandler.cs
using MediatR;
using OrderManagement.Application.Interfaces;
using OrderManagement.Domain.AggregatesModel.OrderAggregate;
using OrderManagement.Domain.ValueObjects;
namespace OrderManagement.Application.Commands.Orders.CreateOrder;
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, int>
{
private readonly IOrderRepository _orderRepository;
private readonly ICustomerRepository _customerRepository;
private readonly IUnitOfWork _unitOfWork;
public CreateOrderHandler(
IOrderRepository orderRepository,
ICustomerRepository customerRepository,
IUnitOfWork unitOfWork)
{
_orderRepository = orderRepository;
_customerRepository = customerRepository;
_unitOfWork = unitOfWork;
}
public async Task<int> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
// 1. 验证客户是否存在
var customer = await _customerRepository.GetByIdAsync(request.CustomerId, cancellationToken);
if (customer == null)
throw new ApplicationException($"客户 {request.CustomerId} 不存在");
// 2. 转换订单项
var orderItems = request.Items.Select(item =>
new OrderItem(
item.ProductId,
item.ProductName,
item.Quantity,
Money.FromDecimal(item.UnitPrice, item.Currency)
)).ToList();
// 3. 创建订单聚合根
var order = new Order(
request.CustomerId,
request.ShippingAddress.ToDomain(),
orderItems,
request.Remark
);
// 4. 持久化
await _orderRepository.AddAsync(order, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
// 5. 返回订单ID
return order.Id;
}
}4. 查询订单
csharp
// OrderManagement.Application/Queries/Orders/GetOrder/GetOrderQuery.cs
using MediatR;
namespace OrderManagement.Application.Queries.Orders.GetOrder;
public record GetOrderQuery(int OrderId) : IRequest<OrderDto?>;
// OrderManagement.Application/Queries/Orders/GetOrder/OrderDto.cs
public class OrderDto
{
public int Id { get; set; }
public Guid OrderNumber { get; set; }
public int CustomerId { get; set; }
public string CustomerName { get; set; }
public string Status { get; set; }
public List<OrderItemDto> Items { get; set; } = new();
public decimal TotalAmount { get; set; }
public string Currency { get; set; }
public AddressDto ShippingAddress { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public string? Remark { get; set; }
}
public class OrderItemDto
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
public decimal TotalPrice { get; set; }
}
public class AddressDto
{
public string Province { get; set; }
public string City { get; set; }
public string District { get; set; }
public string Street { get; set; }
public string ZipCode { get; set; }
}5. 查询处理器
csharp
// OrderManagement.Application/Queries/Orders/GetOrder/GetOrderHandler.cs
using MediatR;
using Microsoft.EntityFrameworkCore;
using OrderManagement.Application.Interfaces;
namespace OrderManagement.Application.Queries.Orders.GetOrder;
public class GetOrderHandler : IRequestHandler<GetOrderQuery, OrderDto?>
{
private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;
public GetOrderHandler(IDbContextFactory<ApplicationDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task<OrderDto?> Handle(GetOrderQuery request, CancellationToken cancellationToken)
{
await using var context = await _contextFactory.CreateDbContextAsync(cancellationToken);
var order = await context.Orders
.Include(o => o.OrderItems)
.Include(o => o.Customer)
.AsNoTracking()
.FirstOrDefaultAsync(o => o.Id == request.OrderId, cancellationToken);
if (order == null)
return null;
return new OrderDto
{
Id = order.Id,
OrderNumber = order.OrderNumber,
CustomerId = order.CustomerId,
CustomerName = order.Customer.Name,
Status = order.Status.ToString(),
Items = order.OrderItems.Select(item => new OrderItemDto
{
ProductId = item.ProductId,
ProductName = item.ProductName,
Quantity = item.Quantity,
UnitPrice = item.UnitPrice.Amount,
TotalPrice = item.TotalPrice.Amount
}).ToList(),
TotalAmount = order.TotalAmount.Amount,
Currency = order.TotalAmount.Currency,
ShippingAddress = new AddressDto
{
Province = order.ShippingAddress.Province,
City = order.ShippingAddress.City,
District = order.ShippingAddress.District,
Street = order.ShippingAddress.Street,
ZipCode = order.ShippingAddress.ZipCode
},
CreatedAt = order.CreatedAt,
UpdatedAt = order.UpdatedAt,
Remark = order.Remark
};
}
}6. 列表查询
csharp
// OrderManagement.Application/Queries/Orders/ListOrders/ListOrdersQuery.cs
using MediatR;
namespace OrderManagement.Application.Queries.Orders.ListOrders;
public record ListOrdersQuery : IRequest<PagedResult<OrderSummaryDto>>
{
public int PageIndex { get; init; } = 1;
public int PageSize { get; init; } = 10;
public int? CustomerId { get; init; }
public string? Status { get; init; }
public DateTime? StartDate { get; init; }
public DateTime? EndDate { get; init; }
public string SortBy { get; init; } = "CreatedAt";
public bool Descending { get; init; } = true;
}
public class OrderSummaryDto
{
public int Id { get; set; }
public Guid OrderNumber { get; set; }
public string CustomerName { get; set; }
public string Status { get; set; }
public decimal TotalAmount { get; set; }
public int ItemCount { get; set; }
public DateTime CreatedAt { get; set; }
}
public class PagedResult<T>
{
public List<T> Items { get; set; } = new();
public int TotalCount { get; set; }
public int PageIndex { get; set; }
public int PageSize { get; set; }
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
public bool HasPrevious => PageIndex > 1;
public bool HasNext => PageIndex < TotalPages;
}🔧 基础设施层实现
1. EF Core DbContext
csharp
// OrderManagement.Infrastructure/Persistence/ApplicationDbContext.cs
using Microsoft.EntityFrameworkCore;
using OrderManagement.Domain.AggregatesModel.OrderAggregate;
using OrderManagement.Domain.AggregatesModel.CustomerAggregate;
namespace OrderManagement.Infrastructure.Persistence;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Order> Orders => Set<Order>();
public DbSet<Customer> Customers => Set<Customer>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 应用所有配置
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
}
}2. Order 配置
csharp
// OrderManagement.Infrastructure/Persistence/Configurations/OrderConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using OrderManagement.Domain.AggregatesModel.OrderAggregate;
namespace OrderManagement.Infrastructure.Persistence.Configurations;
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("Orders");
builder.HasKey(o => o.Id);
builder.Property(o => o.OrderNumber)
.IsRequired();
builder.Property(o => o.Status)
.HasConversion<string>()
.HasMaxLength(50);
builder.OwnsOne(o => o.ShippingAddress, address =>
{
address.Property(a => a.Province).HasMaxLength(50).IsRequired();
address.Property(a => a.City).HasMaxLength(50).IsRequired();
address.Property(a => a.District).HasMaxLength(50).IsRequired();
address.Property(a => a.Street).HasMaxLength(200).IsRequired();
address.Property(a => a.ZipCode).HasMaxLength(20);
});
builder.OwnsOne(o => o.TotalAmount, money =>
{
money.Property(m => m.Amount).HasColumnName("TotalAmount").HasColumnType("decimal(18,2)");
money.Property(m => m.Currency).HasColumnName("Currency").HasMaxLength(3);
});
builder.HasMany(o => o.OrderItems)
.WithOne()
.HasForeignKey(oi => oi.OrderId)
.OnDelete(DeleteBehavior.Cascade);
builder.Navigation(o => o.OrderItems).AutoInclude();
}
}3. OrderItem 配置
csharp
// OrderManagement.Infrastructure/Persistence/Configurations/OrderItemConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using OrderManagement.Domain.AggregatesModel.OrderAggregate;
namespace OrderManagement.Infrastructure.Persistence.Configurations;
public class OrderItemConfiguration : IEntityTypeConfiguration<OrderItem>
{
public void Configure(EntityTypeBuilder<OrderItem> builder)
{
builder.ToTable("OrderItems");
builder.HasKey(oi => oi.Id);
builder.Property(oi => oi.ProductName)
.HasMaxLength(200)
.IsRequired();
builder.OwnsOne(oi => oi.UnitPrice, money =>
{
money.Property(m => m.Amount).HasColumnName("UnitPrice").HasColumnType("decimal(18,2)");
money.Property(m => m.Currency).HasColumnName("Currency").HasMaxLength(3);
});
builder.OwnsOne(oi => oi.TotalPrice, money =>
{
money.Property(m => m.Amount).HasColumnName("TotalPrice").HasColumnType("decimal(18,2)");
money.Property(m => m.Currency).HasColumnName("Currency").HasMaxLength(3);
});
}
}4. Repository 实现
csharp
// OrderManagement.Infrastructure/Repositories/OrderRepository.cs
using Microsoft.EntityFrameworkCore;
using OrderManagement.Application.Interfaces;
using OrderManagement.Domain.AggregatesModel.OrderAggregate;
using OrderManagement.Infrastructure.Persistence;
namespace OrderManagement.Infrastructure.Repositories;
public class OrderRepository : IOrderRepository
{
private readonly ApplicationDbContext _context;
public OrderRepository(ApplicationDbContext context)
{
_context = context;
}
public async Task<Order?> GetByIdAsync(int id, CancellationToken cancellationToken = default)
{
return await _context.Orders
.Include(o => o.OrderItems)
.FirstOrDefaultAsync(o => o.Id == id, cancellationToken);
}
public async Task AddAsync(Order order, CancellationToken cancellationToken = default)
{
await _context.Orders.AddAsync(order, cancellationToken);
}
public void Update(Order order)
{
_context.Orders.Update(order);
}
public void Delete(Order order)
{
_context.Orders.Remove(order);
}
}5. Unit of Work
csharp
// OrderManagement.Application/Interfaces/IUnitOfWork.cs
namespace OrderManagement.Application.Interfaces;
public interface IUnitOfWork : IDisposable
{
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
// OrderManagement.Infrastructure/Persistence/UnitOfWork.cs
namespace OrderManagement.Infrastructure.Persistence;
public class UnitOfWork : IUnitOfWork
{
private readonly ApplicationDbContext _context;
public UnitOfWork(ApplicationDbContext context)
{
_context = context;
}
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
return await _context.SaveChangesAsync(cancellationToken);
}
public void Dispose()
{
_context.Dispose();
}
}📡 API 层实现
1. Orders Controller
csharp
// OrderManagement.API/Controllers/OrdersController.cs
using MediatR;
using Microsoft.AspNetCore.Mvc;
using OrderManagement.Application.Commands.Orders.CreateOrder;
using OrderManagement.Application.Commands.Orders.CancelOrder;
using OrderManagement.Application.Queries.Orders.GetOrder;
using OrderManagement.Application.Queries.Orders.ListOrders;
namespace OrderManagement.API.Controllers;
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
private readonly IMediator _mediator;
public OrdersController(IMediator mediator)
{
_mediator = mediator;
}
/// <summary>
/// 创建订单
/// </summary>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<ActionResult<int>> CreateOrder([FromBody] CreateOrderCommand command, CancellationToken cancellationToken)
{
var orderId = await _mediator.Send(command, cancellationToken);
return CreatedAtAction(nameof(GetOrder), new { id = orderId }, orderId);
}
/// <summary>
/// 获取订单详情
/// </summary>
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<OrderDto>> GetOrder(int id, CancellationToken cancellationToken)
{
var order = await _mediator.Send(new GetOrderQuery(id), cancellationToken);
if (order == null)
return NotFound($"订单 {id} 不存在");
return Ok(order);
}
/// <summary>
/// 获取订单列表
/// </summary>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<PagedResult<OrderSummaryDto>>> ListOrders(
[FromQuery] ListOrdersQuery query,
CancellationToken cancellationToken)
{
var result = await _mediator.Send(query, cancellationToken);
return Ok(result);
}
/// <summary>
/// 取消订单
/// </summary>
[HttpPost("{id}/cancel")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> CancelOrder(int id, [FromBody] CancelOrderRequest request, CancellationToken cancellationToken)
{
try
{
var command = new CancelOrderCommand
{
OrderId = id,
Reason = request.Reason
};
await _mediator.Send(command, cancellationToken);
return NoContent();
}
catch (ApplicationException ex)
{
return BadRequest(new { error = ex.Message });
}
}
}
public record CancelOrderRequest
{
public string Reason { get; init; }
}2. Program.cs
csharp
// OrderManagement.API/Program.cs
using OrderManagement.Application;
using OrderManagement.Infrastructure;
using OrderManagement.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// 添加服务
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 注册 MediatR
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
cfg.RegisterServicesFromAssembly(typeof(Application.DependencyInjection).Assembly);
});
// 注册数据库
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
// 注册依赖
builder.Services.AddApplicationServices();
builder.Services.AddInfrastructureServices();
var app = builder.Build();
// 配置中间件
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();🎨 最佳实践总结
1. CQRS 分离原则
✅ 推荐做法:
- 命令(Commands)用于写操作,返回简单结果(ID、布尔值)
- 查询(Queries)用于读操作,返回 DTO
- 命令和查询使用不同的数据模型
- 查询可以直接使用 DbContext,无需 Repository
❌ 避免做法:
- 在命令中返回完整的实体对象
- 在查询中使用 Repository 模式(过度设计)
- 混合使用命令和查询
2. 领域驱动设计
✅ 推荐做法:
- 聚合根负责维护业务规则
- 使用值对象封装复杂属性(Money、Address)
- 通过领域事件通知外部系统
- 使用工厂方法创建聚合根
❌ 避免做法:
- 在应用层编写业务逻辑
- 直接修改实体的私有字段
- 忽略业务规则的验证
3. 错误处理
✅ 推荐做法:
- 使用领域异常表示业务规则违反
- 在 API 层统一处理异常
- 提供有意义的错误消息
- 使用 HTTP 状态码表达错误类型
csharp
// 全局异常处理中间件
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
var exception = exceptionHandlerPathFeature?.Error;
context.Response.ContentType = "application/json";
context.Response.StatusCode = exception switch
{
DomainException => StatusCodes.Status400BadRequest,
KeyNotFoundException => StatusCodes.Status404NotFound,
_ => StatusCodes.Status500InternalServerError
};
await context.Response.WriteAsync(JsonSerializer.Serialize(new
{
error = exception?.Message,
statusCode = context.Response.StatusCode
}));
});
});4. 性能优化
✅ 推荐做法:
- 查询使用
AsNoTracking() - 使用投影减少数据传输
- 实现分页和筛选
- 使用缓存减少数据库访问
csharp
// 缓存示例
[ResponseCache(Duration = 60)]
[HttpGet("{id}")]
public async Task<ActionResult<OrderDto>> GetOrder(int id)
{
// ...
}📊 测试示例
单元测试
csharp
// tests/OrderManagement.UnitTests/Commands/CreateOrderTests.cs
using FluentAssertions;
using Moq;
using OrderManagement.Application.Commands.Orders.CreateOrder;
using OrderManagement.Application.Interfaces;
using OrderManagement.Domain.AggregatesModel.OrderAggregate;
namespace OrderManagement.UnitTests.Commands;
public class CreateOrderTests
{
private readonly Mock<IOrderRepository> _orderRepositoryMock;
private readonly Mock<ICustomerRepository> _customerRepositoryMock;
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
private readonly CreateOrderHandler _handler;
public CreateOrderTests()
{
_orderRepositoryMock = new Mock<IOrderRepository>();
_customerRepositoryMock = new Mock<ICustomerRepository>();
_unitOfWorkMock = new Mock<IUnitOfWork>();
_handler = new CreateOrderHandler(
_orderRepositoryMock.Object,
_customerRepositoryMock.Object,
_unitOfWorkMock.Object);
}
[Fact]
public async Task Handle_ValidCommand_ShouldCreateOrder()
{
// Arrange
var customer = new Customer("张三", "13800138000");
_customerRepositoryMock.Setup(r => r.GetByIdAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(customer);
var command = new CreateOrderCommand
{
CustomerId = 1,
ShippingAddress = new AddressDto
{
Province = "广东省",
City = "深圳市",
District = "南山区",
Street = "科技园南路",
ZipCode = "518000"
},
Items = new List<OrderItemDto>
{
new() { ProductId = 1, ProductName = "iPhone 15", Quantity = 1, UnitPrice = 7999 }
}
};
// Act
var result = await _handler.Handle(command, CancellationToken.None);
// Assert
result.Should().BeGreaterThan(0);
_orderRepositoryMock.Verify(r => r.AddAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()), Times.Once);
_unitOfWorkMock.Verify(u => u.SaveChangesAsync(It.IsAny<CancellationToken>()), Times.Once);
}
[Fact]
public async Task Handle_InvalidCustomerId_ShouldThrowException()
{
// Arrange
_customerRepositoryMock.Setup(r => r.GetByIdAsync(It.IsAny<int>(), It.IsAny<CancellationToken>()))
.ReturnsAsync((Customer)null);
var command = new CreateOrderCommand
{
CustomerId = 999,
ShippingAddress = new AddressDto { /* ... */ },
Items = new List<OrderItemDto> { /* ... */ }
};
// Act
Func<Task> act = async () => await _handler.Handle(command, CancellationToken.None);
// Assert
await act.Should().ThrowAsync<ApplicationException>()
.WithMessage("*客户*不存在");
}
}🔍 常见问题与解决方案
问题 1:事务管理
症状:订单创建成功,但相关数据未保存
解决方案:
csharp
// 使用 Unit of Work 模式确保事务
public async Task<int> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
try
{
// 业务逻辑...
await _orderRepository.AddAsync(order, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
return order.Id;
}
catch
{
await _unitOfWork.DisposeAsync();
throw;
}
}问题 2:N+1 查询问题
症状:查询订单列表时性能很差
解决方案:
csharp
// ❌ 避免 N+1 查询
var orders = await _context.Orders.ToListAsync();
foreach (var order in orders)
{
order.Customer = await _context.Customers.FindAsync(order.CustomerId);
}
// ✅ 使用 Include 预加载
var orders = await _context.Orders
.Include(o => o.Customer)
.Include(o => o.OrderItems)
.ToListAsync();问题 3:并发冲突
症状:多人同时修改订单导致数据不一致
解决方案:
csharp
// 添加行版本控制
public class Order
{
public byte[] RowVersion { get; set; }
}
// 配置乐观锁
builder.Property(o => o.RowVersion)
.IsRowVersion();
// 处理并发冲突
try
{
await _unitOfWork.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateConcurrencyException ex)
{
throw new ApplicationException("订单已被其他人修改,请刷新后重试");
}📈 扩展方向
1. 添加事件总线
csharp
// 发布领域事件到消息队列
public class OrderCreatedEventHandler : INotificationHandler<OrderCreatedEvent>
{
private readonly IEventBus _eventBus;
public async Task Handle(OrderCreatedEvent notification, CancellationToken cancellationToken)
{
await _eventBus.PublishAsync(new OrderCreatedIntegrationEvent
{
OrderId = notification.OrderId,
OrderNumber = notification.OrderNumber,
Timestamp = notification.CreatedAt
}, cancellationToken);
}
}2. 添加缓存
csharp
// Redis 缓存
public class CachedOrderQueryHandler : IRequestHandler<GetOrderQuery, OrderDto?>
{
private readonly IDistributedCache _cache;
private readonly GetOrderHandler _innerHandler;
public async Task<OrderDto?> Handle(GetOrderQuery request, CancellationToken cancellationToken)
{
var cacheKey = $"order:{request.OrderId}";
var cached = await _cache.GetStringAsync(cacheKey, cancellationToken);
if (cached != null)
return JsonSerializer.Deserialize<OrderDto>(cached);
var result = await _innerHandler.Handle(request, cancellationToken);
if (result != null)
{
await _cache.SetStringAsync(
cacheKey,
JsonSerializer.Serialize(result),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5) },
cancellationToken);
}
return result;
}
}3. 添加审计日志
csharp
// 审计日志实体
public class AuditLog
{
public int Id { get; set; }
public string UserId { get; set; }
public string Action { get; set; }
public string EntityType { get; set; }
public int EntityId { get; set; }
public DateTime Timestamp { get; set; }
public string Changes { get; set; }
}🎯 总结
通过这个完整的 CQRS + MediatR 订单系统实战案例,我们学习了:
- 领域驱动设计:聚合根、值对象、领域事件的建模
- CQRS 模式:命令和查询的职责分离
- MediatR 应用:请求/响应、验证、管道行为
- 分层架构:领域层、应用层、基础设施层、API 层的清晰分离
- 最佳实践:事务管理、性能优化、错误处理
这个案例提供了一个生产级别的参考架构,可以根据实际业务需求进行扩展和调整。