HasQueryFilter只对跟踪查询生效,因其本质是拼接在LINQ表达式树末尾的WHERE条件,而AsNoTracking()不改变表达式构建、原始SQL则完全绕过EF查询管道。

HasQueryFilter 为什么只对跟踪查询生效
EF Core 的 HasQueryFilter 默认只作用于被 DbContext 跟踪的实体(即通过 DbSet<T> 查询出来的结果),对原始 SQL、FromSqlRaw、或 AsNoTracking() 后再过滤的场景不自动应用。这不是 bug,是设计使然:过滤器本质是拼在 LINQ 表达式树末尾的 where 条件,而 AsNoTracking() 不影响表达式构建,但原始 SQL 完全绕过 EF 的查询管道。
常见错误现象:context.Users.AsNoTracking().ToList() 仍返回已软删除用户;或者用 context.Users.FromSqlRaw("SELECT * FROM Users") 拿到未过滤数据。
- 若需在非跟踪场景也强制过滤,得手动补条件,比如
WHERE IsDeleted = 0 -
IgnoreQueryFilters()可临时关闭过滤器,适合管理后台查看全部数据 - 全局过滤器无法用于导航属性的隐式加载(如
Include(u => u.Posts)),Posts 表的过滤器要单独配置
软删除 + 多租户混合过滤怎么写
一个实体常需同时满足「未删除」和「属于当前租户」两个条件,直接链式调用 HasQueryFilter 会覆盖前一个——EF Core 每个实体只允许一个全局过滤器表达式。
正确做法是把多个条件合并进同一个 Lambda:
modelBuilder.Entity<Order>()
.HasQueryFilter(o => !o.IsDeleted && o.TenantId == EF.Property<Guid>("CurrentTenantId"));这里 EF.Property<T>("CurrentTenantId") 是关键:它让 EF 在运行时从 DbContext 实例中读取 CurrentTenantId 属性值(需你在 DbContext 派生类里定义该属性并赋值)。不能直接写 tenantIdVariable,否则会被编译成常量,所有查询都用同一个租户 ID。
- 租户 ID 建议从
HttpContextAccessor或依赖注入容器中获取,在 DbContext 构造时传入或通过拦截器设置 - 软删除字段类型要和数据库一致(如 SQL Server 的
BIT对应 C#bool?或bool),否则生成的 SQL 可能出错 - 如果租户隔离还涉及表名动态切换(如
Orders_Tenant1),HasQueryFilter就不够用了,得配合分表或多 DbContext
HasQueryFilter 导致 Include 关联数据异常怎么办
当你对主实体(如 User)设了 HasQueryFilter,又用 Include(u => u.Orders),EF Core 默认会对 Orders 表也应用同一过滤器——但往往你并不想过滤订单,只想过滤用户。
这通常表现为:明明有未删除订单,user.Orders 却是空集合。原因是 EF 把 User 的过滤条件错误地“传染”给了 Orders 的 JOIN 条件或子查询。
- 最稳妥的解法:给
Order实体单独配自己的HasQueryFilter,比如o => !o.IsDeleted,而不是复用 User 的逻辑 - 避免在 User 过滤器里引用 Order 字段(如
u => !u.IsDeleted && u.Orders.All(o => !o.IsDeleted)),这会导致无法翻译成 SQL - 如果必须跳过关联过滤,可用
ThenInclude后接IgnoreQueryFilters(),但仅限 EF Core 6+,且仅对显式加载有效
测试全局过滤器是否生效的最快方式
别靠肉眼查日志或数据库,直接在单元测试里断言生成的 SQL:
var sql = context.Users.ToQueryString(); // 输出类似:SELECT ... FROM Users AS u WHERE u.IsDeleted = 0
注意 ToQueryString() 是 EF Core 5+ 才有的调试方法,需引用 Microsoft.EntityFrameworkCore.Diagnostics。旧版本可用 context.Users.ToString()(仅显示表达式树,不含 SQL)或启用 EF 日志输出。
- 测试时务必用真实 DbContext 实例(而非 Mock),因为过滤器注册在
OnModelCreating中,Mock 不会执行该逻辑 - 检查迁移脚本是否包含过滤字段的索引——
IsDeleted和TenantId经常需要联合索引,否则查询性能骤降 - 如果发现过滤器没出现在 SQL 里,先确认实体是否被正确注册(
modelBuilder.Entity<T>()是否漏写)、以及是否在OnModelCreating里调用(不是构造函数或别的地方)
全局过滤器看着简单,真正落地时最容易栽在租户上下文传递、关联查询污染、还有测试验证这三个点上。尤其是 EF.Property 的用法,写错一个字符就变成静态值,问题很难复现。


















