规格模式需严格区分toexpression()(供ef转sql)和issatisfiedby()(纯内存执行),and/or组合须手动合并表达式树,避免compile性能雷区及查询翻译失败。

规格模式在 C# 里不是框架内置功能,而是靠 Expression<func bool>></func> 和普通 Func<t bool></t> 的双轨设计撑起来的——用得好能一码三用(内存过滤、数据库查询、单元测试断言),用错就变成“本地跑得欢,线上查不出”。关键不在写得多漂亮,而在分清哪条路走哪条表达式。
Specification 基类必须区分 ToExpression() 和 IsSatisfiedBy()
很多人直接让 IsSatisfiedBy() 调用 ToExpression().Compile(),这是最常见性能雷区。编译表达式树开销大,尤其嵌套多层 .And() 后,每次调用都重新编译,千万级数据下查询延迟翻倍。
-
ToExpression()返回Expression<func bool>></func>,专供 EF Core 的Where()或Linq.ToQueryable()消费,最终转成 SQL -
IsSatisfiedBy()必须是纯内存方法,直接用Func<t bool></t>实现,不碰表达式树,单元测试或 DTO 验证时直接调用 - 基类里别暴露
Expression.Compile(),真要复用逻辑,用私有private bool Evaluate(T item)方法统一实现,两个公开方法都调它
And/Or/Not 组合必须重载 Expression 树,不能只拼 Func
如果 And() 只是把两个 Func<t></t> 用 && 包一层,那这个组合体就永远进不了数据库——EF Core 看不懂委托,只认 Expression。正确做法是手动合并两棵表达式树。
- 用
Expression.Invoke()+Expression.AndAlso()拼接两棵树,再用Expression.Lambda<func>>()</func>封装 - 注意参数绑定:两个子表达式可能用不同参数名,需统一替换为父 Lambda 的参数,否则 EF 报
InvalidOperationException: variable 'x' of type 'T' referenced from scope '' but it is not defined - 推荐直接用开源库
SpecificationPattern(NuGet)的AndSpecification<t></t>,它已处理好变量捕获和类型推导
EF Core 中 Where(spec.ToExpression()) 仍报错?检查 IQueryable 来源
即使 ToExpression() 写对了,context.Products.Where(spec.ToExpression()) 还是抛 System.InvalidOperationException: The LINQ expression could not be translated,大概率是 IQueryable 没走 EF 的管道。
- 确认
context.Products是DbSet<product></product>,不是ToList()后的IEnumerable<product></product>——后者调Where()就强制走内存,表达式树根本没机会翻译 - 避免在表达式里调用非可翻译方法,比如
string.Contains()在旧版 EF Core 中不支持,得换成EF.Functions.Like()或升级到 EF Core 8+ - 复杂业务逻辑(如调外部 API、读配置)绝不能塞进
ToExpression(),这些只能留在IsSatisfiedBy()里
真正难的不是写出能编译的 Specification,而是每次加新规则时,同步更新表达式路径、内存路径、测试用例三处代码。一旦漏掉一个 IsSatisfiedBy() 分支没覆盖,测试通过但线上数据错漏——这种 bug 不报异常,只悄悄吞掉本该过滤掉的数据。











