Skip to content

在 Clean Architecture / DDD 中使用 EF Core ​

概述 ​

Clean Architecture(整洁架构)和 DDD(Domain-Driven Design,领域驱动设计)是现代企业应用开发的主流方法论。EF Core 作为强大的 ORM 框架,可以很好地融入这两种架构模式中。本文将深入探讨如何在 Clean Architecture 和 DDD 项目中有效使用 EF Core。

核心概念回顾 ​

Clean Architecture 核心原则:

  • 依赖倒置: 内层不依赖外层
  • 关注点分离: 每层职责明确
  • 可测试性: 业务逻辑独立于框架

DDD 核心概念:

  • 聚合根(Aggregate Root): 一致性边界
  • 值对象(Value Object): 无身份的对象
  • 领域事件(Domain Event): 领域内的重要事件
  • 仓储(Repository): 聚合的持久化抽象

项目结构 ​

MyApp/
├── src/
│   ├── Domain/                    # 领域层 (最内层)
│   │   ├── Aggregates/           # 聚合根
│   │   ├── ValueObjects/         # 值对象
│   │   ├── Events/               # 领域事件
│   │   └── Interfaces/           # 仓储接口
│   │
│   ├── Application/              # 应用层
│   │   ├── Common/               # 通用接口
│   │   ├── Commands/             # CQRS 命令
│   │   ├── Queries/              # CQRS 查询
│   │   └── DTOs/                 # 数据传输对象
│   │
│   ├── Infrastructure/           # 基础设施层
│   │   ├── Persistence/          # EF Core 实现
│   │   │   ├── Configurations/   # 实体配置
│   │   │   └── Interceptors/     # 拦截器
│   │   └── Services/             # 外部服务实现
│   │
│   └── API/                      # 表示层
│       ├── Controllers/          # API 控制器
│       └── Middleware/           # 中间件
│
└── tests/
    ├── Domain.UnitTests/        # 领域单元测试
    ├── Application.Tests/       # 应用层测试
    └── Integration.Tests/       # 集成测试

1. 领域层设计 ​

聚合根设计 ​

csharp
// Domain/Aggregates/Order.cs
namespace MyApp.Domain.Aggregates;

public class Order : AggregateRoot
{
    private readonly List<OrderItem> _items = new();
    
    // 私有构造函数(EF Core 使用)
    private Order() { }
    
    // 工厂方法
    public static Order Create(string customerId, string shippingAddress)
    {
        if (string.IsNullOrWhiteSpace(customerId))
            throw new DomainException("Customer ID is required");
        
        var order = new Order
        {
            Id = Guid.NewGuid(),
            CustomerId = customerId,
            ShippingAddress = shippingAddress,
            OrderDate = DateTime.UtcNow,
            Status = OrderStatus.Pending
        };
        
        order.AddDomainEvent(new OrderCreatedEvent(order.Id, customerId));
        return order;
    }
    
    // 业务方法
    public void AddItem(Guid productId, int quantity, decimal unitPrice)
    {
        if (quantity <= 0)
            throw new DomainException("Quantity must be positive");
        
        if (Status != OrderStatus.Pending)
            throw new DomainException("Cannot modify completed order");
        
        var existingItem = _items.FirstOrDefault(i => i.ProductId == productId);
        
        if (existingItem != null)
        {
            existingItem.UpdateQuantity(existingItem.Quantity + quantity);
        }
        else
        {
            _items.Add(OrderItem.Create(Id, productId, quantity, unitPrice));
        }
        
        RecalculateTotal();
    }
    
    public void ConfirmOrder()
    {
        if (!_items.Any())
            throw new DomainException("Order must have at least one item");
        
        Status = OrderStatus.Confirmed;
        ConfirmedAt = DateTime.UtcNow;
        
        AddDomainEvent(new OrderConfirmedEvent(Id, CustomerId, TotalAmount));
    }
    
    private void RecalculateTotal()
    {
        TotalAmount = _items.Sum(i => i.Quantity * i.UnitPrice);
    }
    
    // 公共属性
    public Guid Id { get; private set; }
    public string CustomerId { get; private set; } = string.Empty;
    public string ShippingAddress { get; private set; } = string.Empty;
    public DateTime OrderDate { get; private set; }
    public OrderStatus Status { get; private set; }
    public decimal TotalAmount { get; private set; }
    public DateTime? ConfirmedAt { get; private set; }
    
    // 导航属性
    public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
}

// Domain/Aggregates/OrderItem.cs (值对象)
namespace MyApp.Domain.Aggregates;

public class OrderItem : ValueObject
{
    private OrderItem() { }
    
    internal static OrderItem Create(Guid orderId, Guid productId, int quantity, decimal unitPrice)
    {
        return new OrderItem
        {
            OrderId = orderId,
            ProductId = productId,
            Quantity = quantity,
            UnitPrice = unitPrice
        };
    }
    
    internal void UpdateQuantity(int quantity)
    {
        if (quantity <= 0)
            throw new DomainException("Quantity must be positive");
        
        Quantity = quantity;
    }
    
    public Guid OrderId { get; private set; }
    public Guid ProductId { get; private set; }
    public int Quantity { get; private set; }
    public decimal UnitPrice { get; private set; }
    
    protected override IEnumerable<object> GetEqualityComponents()
    {
        yield return OrderId;
        yield return ProductId;
    }
}

领域事件 ​

csharp
// Domain/Events/OrderCreatedEvent.cs
namespace MyApp.Domain.Events;

public record OrderCreatedEvent(Guid OrderId, string CustomerId) : IDomainEvent;

public record OrderConfirmedEvent(Guid OrderId, string CustomerId, decimal TotalAmount) 
    : IDomainEvent;

// Domain/Common/IDomainEvent.cs
namespace MyApp.Domain.Common;

public interface IDomainEvent
{
    DateTime OccurredOn { get; }
}

public abstract class DomainEvent : IDomainEvent
{
    public DateTime OccurredOn { get; } = DateTime.UtcNow;
}

仓储接口 ​

csharp
// Domain/Interfaces/IOrderRepository.cs
namespace MyApp.Domain.Interfaces;

public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(Guid id);
    Task<List<Order>> GetByCustomerIdAsync(string customerId);
    Task AddAsync(Order order);
    void Update(Order order);
    void Delete(Order order);
}

2. 基础设施层实现 ​

DbContext 配置 ​

csharp
// Infrastructure/Persistence/AppDbContext.cs
using Microsoft.EntityFrameworkCore;
using MyApp.Domain.Aggregates;
using MyApp.Domain.Interfaces;

namespace MyApp.Infrastructure.Persistence;

public class AppDbContext : DbContext, IUnitOfWork
{
    private readonly IDomainEventDispatcher? _eventDispatcher;
    
    public AppDbContext(
        DbContextOptions<AppDbContext> options,
        IDomainEventDispatcher? eventDispatcher = null)
        : base(options)
    {
        _eventDispatcher = eventDispatcher;
    }
    
    public DbSet<Order> Orders => Set<Order>();
    public DbSet<OrderItem> OrderItems => Set<OrderItem>();
    public DbSet<Customer> Customers => Set<Customer>();
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 应用所有配置
        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
        
        base.OnModelCreating(modelBuilder);
    }
    
    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        // 发布领域事件
        if (_eventDispatcher != null)
        {
            var domainEvents = ChangeTracker.Entries<IHasDomainEvents>()
                .SelectMany(e => e.Entity.DomainEvents)
                .ToList();
            
            foreach (var @event in domainEvents)
            {
                await _eventDispatcher.DispatchAsync(@event);
            }
            
            // 清除已处理的事件
            foreach (var entry in ChangeTracker.Entries<IHasDomainEvents>())
            {
                entry.Entity.ClearDomainEvents();
            }
        }
        
        return await base.SaveChangesAsync(cancellationToken);
    }
}

实体配置 ​

csharp
// Infrastructure/Persistence/Configurations/OrderConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using MyApp.Domain.Aggregates;

namespace MyApp.Infrastructure.Persistence.Configurations;

public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
    public void Configure(EntityTypeBuilder<Order> builder)
    {
        builder.HasKey(o => o.Id);
        
        builder.Property(o => o.CustomerId)
            .IsRequired()
            .HasMaxLength(50);
        
        builder.Property(o => o.ShippingAddress)
            .IsRequired()
            .HasMaxLength(500);
        
        builder.Property(o => o.Status)
            .IsRequired()
            .HasConversion<string>(); // 枚举转字符串存储
        
        builder.Property(o => o.TotalAmount)
            .HasColumnType("decimal(18,2)");
        
        // 配置 owned entity (订单项)
        builder.OwnsMany(o => o.Items, itemBuilder =>
        {
            itemBuilder.WithOwner().HasForeignKey(i => i.OrderId);
            
            itemBuilder.Property(i => i.ProductId)
                .IsRequired();
            
            itemBuilder.Property(i => i.Quantity)
                .IsRequired();
            
            itemBuilder.Property(i => i.UnitPrice)
                .HasColumnType("decimal(18,2)")
                .IsRequired();
            
            itemBuilder.ToTable("OrderItems");
        });
        
        // 忽略领域事件(不持久化)
        builder.Ignore(o => o.DomainEvents);
    }
}

仓储实现 ​

csharp
// Infrastructure/Persistence/Repositories/OrderRepository.cs
using Microsoft.EntityFrameworkCore;
using MyApp.Domain.Aggregates;
using MyApp.Domain.Interfaces;

namespace MyApp.Infrastructure.Persistence.Repositories;

public class OrderRepository : IOrderRepository
{
    private readonly AppDbContext _context;
    
    public OrderRepository(AppDbContext context)
    {
        _context = context;
    }
    
    public async Task<Order?> GetByIdAsync(Guid id)
    {
        return await _context.Orders
            .Include(o => o.Items)
            .FirstOrDefaultAsync(o => o.Id == id);
    }
    
    public async Task<List<Order>> GetByCustomerIdAsync(string customerId)
    {
        return await _context.Orders
            .Include(o => o.Items)
            .Where(o => o.CustomerId == customerId)
            .OrderByDescending(o => o.OrderDate)
            .ToListAsync();
    }
    
    public async Task AddAsync(Order order)
    {
        await _context.Orders.AddAsync(order);
    }
    
    public void Update(Order order)
    {
        _context.Orders.Update(order);
    }
    
    public void Delete(Order order)
    {
        _context.Orders.Remove(order);
    }
}

3. 应用层(CQRS + MediatR) ​

命令处理器 ​

csharp
// Application/Commands/CreateOrderCommand.cs
using MediatR;

namespace MyApp.Application.Commands;

public record CreateOrderCommand(
    string CustomerId,
    string ShippingAddress,
    List<OrderItemRequest> Items) : IRequest<Guid>;

public record OrderItemRequest(Guid ProductId, int Quantity, decimal UnitPrice);

// Application/Commands/CreateOrderCommandHandler.cs
namespace MyApp.Application.Commands;

public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IUnitOfWork _unitOfWork;
    
    public CreateOrderCommandHandler(
        IOrderRepository orderRepository,
        IUnitOfWork unitOfWork)
    {
        _orderRepository = orderRepository;
        _unitOfWork = unitOfWork;
    }
    
    public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        // 使用工厂方法创建订单(领域逻辑)
        var order = Order.Create(request.CustomerId, request.ShippingAddress);
        
        // 添加订单项(领域逻辑)
        foreach (var item in request.Items)
        {
            order.AddItem(item.ProductId, item.Quantity, item.UnitPrice);
        }
        
        // 持久化
        await _orderRepository.AddAsync(order);
        await _unitOfWork.SaveChangesAsync(cancellationToken);
        
        // 返回订单 ID
        return order.Id;
    }
}

查询处理器 ​

csharp
// Application/Queries/GetOrderQuery.cs
using MediatR;

namespace MyApp.Application.Queries;

public record GetOrderQuery(Guid OrderId) : IRequest<OrderDto>;

public record OrderDto(
    Guid Id,
    string CustomerId,
    string ShippingAddress,
    DateTime OrderDate,
    string Status,
    decimal TotalAmount,
    List<OrderItemDto> Items);

public record OrderItemDto(Guid ProductId, int Quantity, decimal UnitPrice);

// Application/Queries/GetOrderQueryHandler.cs
namespace MyApp.Application.Queries;

public class GetOrderQueryHandler : IRequestHandler<GetOrderQuery, OrderDto?>
{
    private readonly AppDbContext _context;
    
    public GetOrderQueryHandler(AppDbContext context)
    {
        _context = context;
    }
    
    public async Task<OrderDto?> Handle(GetOrderQuery request, CancellationToken cancellationToken)
    {
        return await _context.Orders
            .Where(o => o.Id == request.OrderId)
            .Select(o => new OrderDto(
                o.Id,
                o.CustomerId,
                o.ShippingAddress,
                o.OrderDate,
                o.Status.ToString(),
                o.TotalAmount,
                o.Items.Select(i => new OrderItemDto(
                    i.ProductId,
                    i.Quantity,
                    i.UnitPrice)).ToList()))
            .FirstOrDefaultAsync(cancellationToken);
    }
}

4. 领域事件处理 ​

事件分发器 ​

csharp
// Infrastructure/Events/DomainEventDispatcher.cs
namespace MyApp.Infrastructure.Events;

public interface IDomainEventDispatcher
{
    Task DispatchAsync(IDomainEvent domainEvent);
}

public class DomainEventDispatcher : IDomainEventDispatcher
{
    private readonly IServiceProvider _serviceProvider;
    
    public DomainEventDispatcher(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public async Task DispatchAsync(IDomainEvent domainEvent)
    {
        // 查找所有处理该事件的处理器
        var eventType = domainEvent.GetType();
        var handlerType = typeof(IDomainEventHandler<>).MakeGenericType(eventType);
        
        var handlers = _serviceProvider.GetServices(handlerType);
        
        foreach (var handler in handlers)
        {
            var method = handlerType.GetMethod("HandleAsync");
            if (method != null)
            {
                await (Task)method.Invoke(handler, new object[] { domainEvent })!;
            }
        }
    }
}

// 事件处理器接口
public interface IDomainEventHandler<in TEvent> where TEvent : IDomainEvent
{
    Task HandleAsync(TEvent domainEvent);
}

// 示例: 订单创建事件处理器
public class OrderCreatedEventHandler : IDomainEventHandler<OrderCreatedEvent>
{
    private readonly IEmailService _emailService;
    
    public OrderCreatedEventHandler(IEmailService emailService)
    {
        _emailService = emailService;
    }
    
    public async Task HandleAsync(OrderCreatedEvent domainEvent)
    {
        // 发送确认邮件
        await _emailService.SendOrderConfirmationAsync(domainEvent.OrderId);
    }
}

5. 高级技巧 ​

软删除实现 ​

csharp
// Domain/Common/ISoftDelete.cs
namespace MyApp.Domain.Common;

public interface ISoftDelete
{
    bool IsDeleted { get; set; }
    DateTime? DeletedAt { get; set; }
}

// Infrastructure/Interceptors/SoftDeleteInterceptor.cs
using Microsoft.EntityFrameworkCore.Diagnostics;

namespace MyApp.Infrastructure.Interceptors;

public class SoftDeleteInterceptor : SaveChangesInterceptor
{
    public override InterceptionResult<int> SavingChanges(
        DbContextEventData eventData,
        InterceptionResult<int> result)
    {
        if (eventData.Context != null)
        {
            var entries = eventData.Context.ChangeTracker
                .Entries<ISoftDelete>()
                .Where(e => e.State == EntityState.Deleted);
            
            foreach (var entry in entries)
            {
                entry.State = EntityState.Modified;
                entry.Entity.IsDeleted = true;
                entry.Entity.DeletedAt = DateTime.UtcNow;
            }
        }
        
        return base.SavingChanges(eventData, result);
    }
}

// 注册拦截器
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
{
    options.UseSqlServer(connectionString)
           .AddInterceptors(sp.GetRequiredService<SoftDeleteInterceptor>());
});

审计日志 ​

csharp
// Domain/Common/IAuditable.cs
namespace MyApp.Domain.Common;

public interface IAuditable
{
    string CreatedBy { get; set; }
    DateTime CreatedAt { get; set; }
    string? ModifiedBy { get; set; }
    DateTime? ModifiedAt { get; set; }
}

// Infrastructure/Interceptors/AuditInterceptor.cs
public class AuditInterceptor : SaveChangesInterceptor
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    
    public AuditInterceptor(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
    
    public override InterceptionResult<int> SavingChanges(
        DbContextEventData eventData,
        InterceptionResult<int> result)
    {
        var currentUser = _httpContextAccessor.HttpContext?.User?.Identity?.Name ?? "System";
        var now = DateTime.UtcNow;
        
        if (eventData.Context != null)
        {
            foreach (var entry in eventData.Context.ChangeTracker.Entries<IAuditable>())
            {
                switch (entry.State)
                {
                    case EntityState.Added:
                        entry.Entity.CreatedBy = currentUser;
                        entry.Entity.CreatedAt = now;
                        break;
                    
                    case EntityState.Modified:
                        entry.Entity.ModifiedBy = currentUser;
                        entry.Entity.ModifiedAt = now;
                        break;
                }
            }
        }
        
        return base.SavingChanges(eventData, result);
    }
}

6. 测试策略 ​

领域单元测试 ​

csharp
// Domain.UnitTests/OrderTests.cs
namespace MyApp.Domain.UnitTests;

public class OrderTests
{
    [Fact]
    public void CreateOrder_WithValidData_ShouldSucceed()
    {
        // Act
        var order = Order.Create("CUST001", "123 Main St");
        
        // Assert
        Assert.NotNull(order);
        Assert.Equal("CUST001", order.CustomerId);
        Assert.Equal(OrderStatus.Pending, order.Status);
        Assert.Single(order.DomainEvents); // 触发了 OrderCreatedEvent
    }
    
    [Fact]
    public void AddItem_ToPendingOrder_ShouldSucceed()
    {
        // Arrange
        var order = Order.Create("CUST001", "123 Main St");
        
        // Act
        order.AddItem(Guid.NewGuid(), 2, 50m);
        
        // Assert
        Assert.Single(order.Items);
        Assert.Equal(100m, order.TotalAmount);
    }
    
    [Fact]
    public void AddItem_ToCompletedOrder_ShouldThrowException()
    {
        // Arrange
        var order = Order.Create("CUST001", "123 Main St");
        order.ConfirmOrder();
        
        // Act & Assert
        Assert.Throws<DomainException>(() => 
            order.AddItem(Guid.NewGuid(), 1, 50m));
    }
}

集成测试 ​

csharp
// Integration.Tests/OrderIntegrationTests.cs
namespace MyApp.IntegrationTests;

public class OrderIntegrationTests : IClassFixture<IntegrationTestFactory>
{
    private readonly IntegrationTestFactory _factory;
    
    public OrderIntegrationTests(IntegrationTestFactory factory)
    {
        _factory = factory;
    }
    
    [Fact]
    public async Task CreateOrder_ThroughMediator_ShouldPersistToDatabase()
    {
        // Arrange
        using var scope = _factory.Services.CreateScope();
        var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
        var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        
        var command = new CreateOrderCommand(
            "CUST001",
            "123 Main St",
            new List<OrderItemRequest>
            {
                new(Guid.NewGuid(), 2, 50m),
                new(Guid.NewGuid(), 1, 100m)
            });
        
        // Act
        var orderId = await mediator.Send(command);
        
        // Assert
        var order = await context.Orders.FindAsync(orderId);
        Assert.NotNull(order);
        Assert.Equal(200m, order.TotalAmount);
    }
}

总结 ​

Clean Architecture + DDD 的优势 ​

  • ✅ 高度可维护: 清晰的职责分离
  • ✅ 易于测试: 领域逻辑独立
  • ✅ 灵活扩展: 可以轻松替换技术栈
  • ✅ 业务对齐: 代码反映业务语言

挑战 ​

  • ⚠️ 学习曲线: 需要理解多个概念
  • ⚠️ 初期复杂度: 小项目可能过度设计
  • ⚠️ 样板代码: 需要编写较多基础代码

最佳实践 ​

  1. 从简单开始: 不要一开始就引入所有概念
  2. 渐进式演进: 随着业务发展逐步完善架构
  3. 保持一致: 团队达成共识并遵守规范
  4. 务实至上: 根据实际需求调整理论

Clean Architecture 和 DDD 不是银弹,但对于复杂的企业级应用,它们提供了强大的工具来管理复杂度并保持代码质量!

基于 MIT 许可发布