在领域驱动设计(DDD)中,领域层是应用程序的核心,它包含了业务逻辑以及对现实世界概念进行建模的实体。领域建模的一个关键方面是使用导航属性定义实体之间的关系。
在 C# 和 Entity Framework Core(EF Core)中,导航属性允许您在实体之间遍历关系。然而,使用不当可能导致性能问题、紧耦合甚至循环引用。
本文探讨了在领域层中实现导航属性的最佳实践,同时保持设计的清晰和可维护性。
理解导航属性 EF Core 中的导航属性定义了实体之间的关系,例如:
示例:
public class Order
{
public int Id { get; set; }
public string OrderNumber { get; set; }
public ICollection<OrderItem> Items { get; set; } // 一对多
}
public class OrderItem
{
public int Id { get; set; }
public string ProductName { get; set; }
public int OrderId { get; set; } // 外键
public Order Order { get; set; } // 导航回 Order
}
导航属性的最佳实践
谨慎使用延迟加载 EF Core 支持延迟加载,但如果使用不当,可能导致 N+1 查询问题。
✅ 应该做:
使用 virtual 关键字实现延迟加载(如果需要):
public virtual ICollection<OrderItem> Items { get; set; }
❌ 应避免:
在 Web 应用程序中过度使用延迟加载(更推荐使用 .Include() 进行预先加载)。
使用显式加载以获得更好的控制 考虑使用显式加载代替延迟加载:
var order = dbContext.Orders.First();
dbContext.Entry(order).Collection(o => o.Items).Load();
在不需要时避免双向导航
并非所有关系都需要双向导航。如果 OrderItem 不需要引用 Order,则可以省略:
public class OrderItem
{
public int Id { get; set; }
public string ProductName { get; set; }
public int OrderId { get; set; } // 仅保留外键
// public Order Order { get; set; } 不需要导航回 Order
}
使用私有 Set 器实现不可变性 为了强制执行领域规则,限制属性修改:
public IReadOnlyCollection<OrderItem> Items { get; private set; } = new List<OrderItem>();
小心处理聚合根 在 DDD 中,聚合根控制对子实体的访问。避免暴露破坏封装的导航属性。
public class Order : AggregateRoot
{
private readonly List<OrderItem> _items = new();
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
public void AddItem(OrderItem item)
{
// 在添加前验证业务规则
_items.Add(item);
}
}
性能考量
注意 N+1 查询问题 延迟加载可能触发多个数据库查询。请使用:
.Include()).Select())仅加载所需的数据。考虑使用 DTO 代替直接暴露实体 直接返回领域实体可能导致数据过度获取。为 API 使用 DTO(数据传输对象):
public class OrderDto
{
public int Id { get; set; }
public List<OrderItemDto> Items { get; set; }
}
避免循环引用
如果 Order 引用 User 并且 User 引用 Order,JSON 序列化可能会失败。
✅ 解决方案:
[JsonIgnore]。modelBuilder.Entity<Order>()
.HasOne(o => o.User)
.WithMany()
.OnDelete(DeleteBehavior.Restrict);
测试导航属性 确保导航属性在单元测试中按预期工作:
[Fact]
public void Order_Should_Have_Items()
{
var order = new Order();
order.AddItem(new OrderItem("Product1"));
Assert.Single(order.Items);
}
即使没有 EF Core,为何还要使用导航属性? 导航属性不仅仅是 ORM(EF Core)的一个功能——它们是一种领域建模工具。
使用导航属性的关键理由:
✅ 表现力:清晰定义领域实体之间的关系。
✅ 封装性:控制实体如何交互(例如,使用 Order.AddItem() 而不是直接操作列表)。
✅ 业务逻辑强制执行:确保不变性(例如,一个 OrderItem 不能没有 Order 而存在)。
✅ 可测试性:更容易在没有数据库的情况下模拟和测试领域行为。
何时应避免使用导航属性(在没有 EF Core 的情况下)? ❌ 如果性能至关重要(例如,在高负载系统中,对象遍历成本高昂)。 ❌ 如果使用微服务架构(更倾向于通过 ID 进行松耦合引用)。 ❌ 如果使用 NoSQL 数据库(其关系处理方式不同)。