仓储模式仅在需替换数据源、提升可测性或明确领域边界时才必要;泛型irepository仅适用于极简crud,真实业务应定义具名接口(如iorderrepository)以承载业务语义、事务约束与查询逻辑。

仓储模式不是必须加的抽象层,只有当你需要替换数据源、做可测业务逻辑、或明确划分领域边界时,它才真正起作用。盲目套用泛型 IRepository<t></t> 反而会让查询语义丢失、事务失控、测试变假。
什么时候该定义具体仓储接口,而不是泛型 IRepository
泛型接口只适合极简 CRUD 场景,真实业务中几乎立刻会破功。比如订单状态流转要校验库存、预占、发事件——这些没法塞进 Add 或 Update 里。
- 当方法名需要表达业务意图时(如
GetActiveOrdersByCustomerSince(DateTime)),必须用具体接口IOrderRepository - 当多个聚合根的数据访问策略差异大(
Product允许最终一致性,Payment必须强事务),泛型无法承载不同生命周期和隔离级别 - 当你发现服务层反复写
_context.Orders.Include(x => x.Items).Where(...).ToList(),说明查询逻辑已泄漏,该收口到具体仓储了 - 如果只是增删改查且不换存储、不写单元测试,直接用 EF Core 的
DbSet<t></t>更轻量
EF Core 下实现仓储最容易漏掉的三件事
很多示例代码跑得通,但上线后并发一高就崩,问题常出在底层生命周期和事务控制上。
-
DbContext必须注册为ServiceLifetime.Scoped,不能是Singleton;否则多请求共享上下文,SaveChangesAsync会抛InvalidOperationException: A second operation started on this context before a previous operation completed - 仓储构造函数里不能 new
DbContext,必须通过 DI 获取——否则和IUnitOfWork拿不到同一个实例,跨仓储事务失效 - 别在仓储里手动调
_context.Database.BeginTransaction();要用ITransactionScopeFactory或由UnitOfWork.SaveChangesAsync()统一管控,否则嵌套调用时事务边界混乱
为什么 IQueryable 不该从仓储方法返回
返回 IQueryable<t></t> 看似灵活,实则把查询执行权交给了服务层,极易引发 N+1、未加 AsNoTracking() 导致内存暴涨、Include 漏写等线上问题。
- 仓储内部应完成必要
Include、AsNoTracking、AsSplitQuery等策略,返回IEnumerable<t></t>或Task<list>></list>强制查询边界 - 若真需动态条件组合,提供
Expression<func bool>></func>参数的FindAsync方法,但执行仍由仓储内完成(.Where(predicate).ToListAsync()) - 服务层拿到的是结果集,不是“可拼接的查询对象”,避免误用
.Count()触发全表扫描
最常被忽略的点是:仓储不是为了“看起来像 DDD”,而是为了应对变化。当你还没遇到要换数据库、没写过一个 Mock 测试、没被 N+1 拖垮过接口响应时间,那现在加仓储,大概率是在给未来自己埋坑。











