Specification基类应持Expression字段而非Func,通过Expression.AndAlso等组合表达式树,并提供ToExpression()供EF Core直接消费;使用时须调用.Where(spec.ToExpression()),避免闭包、Invoke及内存执行,关联查询需分层设计且仓储层统一处理。

Specification 基类怎么写才支持组合与延迟执行
规约模式的核心不是“写个接口”,而是让 IsSatisfiedBy 能安全组合(And、Or、Not)且不提前触发数据库查询。直接在内存里用 Func 实现会断掉 EF Core 的表达式树翻译,导致全表拉到内存再过滤。
正确做法是基于 Expression 构建可组合的规约:
- 基类必须持有一个
Expression字段,而非> Func -
And/Or方法不能返回新委托,而要调用Expression.AndAlso或Expression.OrElse合并表达式树 - 提供
ToExpression()方法暴露原始表达式,供 EF Core 的Where()直接消费 - 避免在构造函数里执行任何数据访问或复杂逻辑——规约本身只是“条件描述”,不是“条件执行”
示例:public abstract class Specification
EF Core 中怎么把 Specification 用进 IQueryable.Where
关键不是“怎么传进去”,而是“怎么不破坏表达式树”。常见错误是先调用 spec.IsSatisfiedBy(entity)(返回 bool),再用这个 bool 去过滤——这会让整个查询变成内存遍历。
正确链路只有一条:从 IQueryable 出发,用 .Where(spec.ToExpression()):
- 确保
spec.ToExpression()返回的是未编译的Expression,不是Func - 如果规约内部引用了外部变量(比如
minPrice),必须用表达式参数替换,不能捕获闭包(EF Core 翻译不了closure) - 复合规约(如
new ActiveUserSpec().And(new PremiumUserSpec()))必须保证合并后的表达式仍可被 EF Core 解析——避免使用Expression.Invoke或自定义方法调用 - 调试时可打印
spec.ToExpression().ToString()看生成的表达式是否含Convert(...)或Call(...),这些大概率无法翻译
Specification 继承体系里怎么处理跨表关联查询
当规约需要检查导航属性(如 User.Orders.Count > 0)时,EF Core 对表达式树的支持很敏感。直接写 x => x.Orders.Any(o => o.Status == "Shipped") 通常可行,但嵌套过深或含聚合(Sum、Avg)就容易失败。
应对策略不是硬扛,而是分层设计:
- 基础规约只操作根实体字段(
User.IsActive、User.CreatedAt) - 关联规约单独封装,命名体现范围,如
OrderHasShippedItemSpecification,并在使用时显式.Include(u => u.Orders).ThenInclude(o => o.Items) - 避免在规约内部做
.ToList()或.AsEnumerable()—— 这等于主动放弃服务端过滤 - 对 EF Core 不支持的运算(如
string.Contains在某些数据库大小写敏感问题),改用EF.Functions.Like或EF.Functions.ILike显式指定
为什么 Specification 在仓储层调用比在应用服务层更安全
不是“应该放哪”,而是“放错地方会导致什么”。把规约实例传给应用服务,再由它拼 IQueryable,很容易无意中触发 ToList()、Count() 或 First(),后续所有规约都变成内存操作。
推荐做法是仓储接口直接暴露规约感知方法:
-
IRepository,内部统一做.ListAsync(Specification spec) query.Where(spec.ToExpression()).ToListAsync() - 仓储实现里控制
Include行为——例如UserWithOrdersSpec可触发自动Include(u => u.Orders),靠规约类型判断,而不是靠调用方传一堆include参数 - 禁止仓储方法返回
IQueryable给上层——这等于把表达式树泄漏出去,失控风险极高 - 测试时直接 mock 仓储,传入不同规约,断言最终生成的 SQL 是否含预期
WHERE条件(用 EF Core 的DbContextOptionsBuilder.EnableSensitiveDataLogging(true)查看)
最常被忽略的一点:规约对象本身应该是无状态的、不可变的。一旦你在某个规约里缓存了 DateTime.Now 或用户 ID,它就不再能被复用或安全组合。











