Skip to content

实现完整 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 订单系统实战案例,我们学习了:

  1. 领域驱动设计:聚合根、值对象、领域事件的建模
  2. CQRS 模式:命令和查询的职责分离
  3. MediatR 应用:请求/响应、验证、管道行为
  4. 分层架构:领域层、应用层、基础设施层、API 层的清晰分离
  5. 最佳实践:事务管理、性能优化、错误处理

这个案例提供了一个生产级别的参考架构,可以根据实际业务需求进行扩展和调整。

Released under the CC BY-SA 4.0 License.