仓储模式非必需,仅在需隔离领域逻辑与数据访问、提升可测试性或切换存储实现时才真正有用;盲目套用会增加抽象与维护成本。

仓储模式不是必须的,但当你需要隔离领域逻辑与数据访问细节、做可测试的业务层、或切换存储实现(比如从 SQL 切到内存/NoSQL)时,它才真正有用。盲目套用只会增加抽象层数和维护成本。
什么时候该定义 IRepository 接口
接口不是为了“看起来像 DDD”,而是为了解耦和替换。只有当以下条件至少满足一个时,才值得抽象:
- 你明确计划在测试中用
InMemoryRepository替换数据库访问 - 你有多个聚合根,且它们的数据访问行为差异大(比如
Order需要事务,Product允许最终一致性) - 你正在对接非 EF Core 的持久化方案(如 Dapper + 自定义 SQL、MongoDB Driver、EventStoreDB)
- 你发现业务服务里频繁出现
_context.Orders.Where(...).Include(...).ToList()这类硬编码查询,且跨多个方法重复
别为了“统一 CRUD”而定义泛型 IRepository——它几乎无法表达真实业务语义,比如 GetActiveOrdersByCustomerSince() 这种方法放不进泛型接口里,最后还是得加具体仓储。
OrderRepository 为什么通常比 IRepository 更合理
真实业务中,聚合根的操作很少是纯 CRUD。比如订单可能涉及状态机校验、库存预占、事件发布等。把这些逻辑塞进泛型接口会破坏单一职责,也掩盖了领域意图。
- 用具体仓储名(如
IOrderRepository)能清晰表达“这是订单专属的数据契约” - 接口方法名应反映业务动作:
FindConfirmedOrderByCode(string code)比GetById(Guid id)更具上下文 - EF Core 中,
IQueryable不该暴露给上层——它会让业务代码意外触发 N+1 或未优化查询;应在仓储内部完成Include和AsNoTracking等策略 - 如果仓储方法返回
IEnumerable而非IQueryable,就强制了查询边界,避免延迟执行泄漏到服务层
EF Core 下实现仓储时最容易漏掉的三件事
很多示例代码只写了个 SaveChangesAsync() 就算完,实际落地会踩坑:
-
DbContext生命周期必须是 scoped(而非 singleton),否则并发写入会出错;注册时用services.AddDbContext(ServiceLifetime.Scoped) - 仓储构造函数里别直接 new
DbContext——必须通过 DI 获取,否则无法共享同一 Unit of Work 实例,事务会失效 - 不要在仓储里手动调用
BeginTransaction():EF Core 的UseTransaction()或更高层的ITransactionScopeFactory才是可控方式;否则嵌套调用时容易抛InvalidOperationException: A second operation started on this context before a previous operation completed
依赖注入时,仓储和领域服务的注册顺序有影响吗
没有直接影响,但间接决定事务和生命周期是否对齐。关键点在于:
- 所有仓储和使用它们的服务,必须注册为
Scoped(默认),才能共享同一个DbContext实例 - 如果你把某个仓储注册成
Transient,它每次被注入都会拿到新的DbContext,导致SaveChanges无法跨仓储生效 - 领域服务(如
IOrderService)若含业务级缓存或状态,也必须是Scoped,否则在一次 HTTP 请求中可能拿到不同仓储实例,状态不一致
最常被忽略的是:仓储接口和实现类在 DI 容器里注册时,没检查是否用了同一生命周期——尤其当团队多人协作、各自注册时,很容易一个写 AddScoped,另一个却写了 AddTransient,问题只在高并发下才暴露。











