Appearance
在 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 的优势
- ✅ 高度可维护: 清晰的职责分离
- ✅ 易于测试: 领域逻辑独立
- ✅ 灵活扩展: 可以轻松替换技术栈
- ✅ 业务对齐: 代码反映业务语言
挑战
- ⚠️ 学习曲线: 需要理解多个概念
- ⚠️ 初期复杂度: 小项目可能过度设计
- ⚠️ 样板代码: 需要编写较多基础代码
最佳实践
- 从简单开始: 不要一开始就引入所有概念
- 渐进式演进: 随着业务发展逐步完善架构
- 保持一致: 团队达成共识并遵守规范
- 务实至上: 根据实际需求调整理论
Clean Architecture 和 DDD 不是银弹,但对于复杂的企业级应用,它们提供了强大的工具来管理复杂度并保持代码质量!