ispecification必须是泛型的,因为需复用在不同实体上,ispecification的t决定作用域;非泛型会导致ef core无法推导表达式树类型而抛异常,且丧失命名即契约、可测试、可组合等核心价值。

规约模式在 C# 里不是“加个接口就能用”,它必须基于 Expression<func bool>></func> 构建,否则 EF Core 会静默降级为客户端过滤——数据一过万就内存爆掉。
为什么 ISpecification 必须是泛型的
非泛型 ISpecification 会导致 EF Core 无法推导表达式树类型,抛出 InvalidOperationException: The LINQ expression could not be translated。根本原因在于:ToExpression() 返回的 Expression<func bool>></func> 无法映射到具体实体字段,EF Core 翻译器直接放弃。
-
ISpecification<user></user>能让编译器和 EF Core 都知道参数类型是User,从而正确绑定x.Name、x.CreatedAt等成员访问 - 必须加
where T : class约束,否则值类型(如int)传入时,EF Core 在构建查询时可能抛NotSupportedException - 不要手写空泛型接口,直接继承基类
Specification<t></t>,它已封装好And/Or的安全合并逻辑
And/Or 组合时 Expression 参数名不一致怎么办
两个规约 spec1.ToExpression() 和 spec2.ToExpression() 的参数名很可能不同(比如一个是 x,另一个是 y),直接调用 Expression.AndAlso 会触发 ArgumentException: Parameter 'x' not found in expression。
- 必须统一参数引用:用左侧表达式的
Parameters[0]替换右侧表达式中所有参数节点 - 不能手动 new
ParameterExpression,否则 EF Core 认不出这是同一变量 - 推荐使用轻量
ExpressionVisitor子类(如ExpressionParameterReplacer)做替换,而不是字符串拼接或反射 - 组合后调用
spec1.And(spec2).ToExpression().ToString()检查输出,确认没有Convert()或Call()—— 这些基本无法翻译成 SQL
EF Core 中 Where(spec.ToExpression()) 为什么不能写成 Where(x => spec.IsSatisfiedBy(x))
后者是 Func<t bool></t>,执行时 EF Core 已经失去表达式树上下文,只能把整张表拉到内存再遍历判断,等于废掉了 ORM 的核心能力。
-
IsSatisfiedBy只应在单元测试或内存集合(如List<t>.Where()</t>)中使用,绝不能出现在 IQueryable 链路里 - 所有外部变量(如
minPrice、tenantId)必须通过表达式树常量注入,不能捕获闭包 ——EF Core 翻译不了 lambda 里的局部变量引用 - 调试时开启
options.LogTo(Console.WriteLine),确认生成的 SQL 含有预期的WHERE条件,而不是SELECT * FROM ...后跟一堆.Where(...) - 关联查询(如
User.Orders.Any(o => o.Status == "Shipped"))要谨慎,嵌套过深或含Sum/Avg容易触发客户端求值
Specification 怎么和 EF Core 拦截器配合
拦截器看不到 ISpecification<t></t> 对象本身,它只处理拼合后的最终 Expression。想让拦截器感知规约上下文,得靠“带标记的查询”。
- 在
ISpecification<t></t>接口里加string Tag { get; }或Type SpecType { get; },用于标识业务语义(如"TenantFilter"、"AuditLogQuery") - 写扩展方法
IQueryable<t>.WithSpecification(ISpecification<t> spec)</t></t>,把spec.Tag存进QueryContext.UserState或自定义DbContext属性 - 在
IAsyncQueryProviderInterceptor.ExecuteAsync里从queryContext.Context提取标记,决定是否注入租户过滤、打日志或生成缓存键 - 别试图在拦截器里重新解析原始规约逻辑 —— 表达式树已扁平化,原始结构不可逆
最易被忽略的点:规约对象本身不该持有 DbContext 或执行任何 IO;它的唯一职责是描述条件。一旦你在 Specification<t></t> 构造函数里查数据库、读配置或调远程 API,整个模式就退化成反模式。











