EF Core 软删除需用 HasQueryFilter 配置全局查询过滤器(如 !x.IsDeleted),配合 SaveChanges 拦截将 Delete 转为更新,并通过 IgnoreQueryFilters() 临时绕过;字段优选 bool,注意表达式可翻译性与团队规范。

EF Core 的 HasQueryFilter 怎么加软删除条件
全局查询过滤器的核心就是 HasQueryFilter,它让 EF Core 在生成 SQL 时自动拼上指定条件。软删除通常靠一个 IsDeleted 布尔字段或 DeletedAt 可空时间戳,最常用的是前者。
在 OnModelCreating 中为实体配置过滤器:
modelBuilder.Entity() .HasQueryFilter(x => !x.IsDeleted);
注意:这个表达式必须是可被 EF Core 翻译成 SQL 的纯 LINQ 表达式,不能调用本地方法、不能访问闭包变量(如 var now = DateTime.Now),否则运行时报 InvalidOperationException: The LINQ expression could not be translated。
- 如果实体有继承关系(比如
BaseEntity含IsDeleted),可对基类统一配置:modelBuilder.Entity().HasQueryFilter(x => !x.IsDeleted) - 多个实体需分别调用
HasQueryFilter,EF Core 不支持“对所有实体自动应用” - 过滤器只影响查询(
Find、Where、Include等),不影响SaveChanges或原始 SQL
怎么绕过全局过滤器查已删除数据
有时需要查看或恢复软删除记录,这时得临时禁用过滤器。EF Core 提供了 IgnoreQueryFilters() 方法:
context.Orders.IgnoreQueryFilters().Where(x => x.IsDeleted).ToList();
它只作用于当前查询链,不影响其他查询。但要注意:
-
IgnoreQueryFilters()必须放在查询构建的最前面(比如context.Xxx.IgnoreQueryFilters().Where(...)),放错位置(如.Where(...).IgnoreQueryFilters())会失效 - 它不递归跳过导航属性的过滤器,若
Order包含Customer,而Customer也有过滤器,Include(x => x.Customer)仍会受其约束 - 若需完全绕过(包括关联实体),得对每个导航属性单独调用
IgnoreQueryFilters(),或改用投影(Select)避免加载实体
软删除字段类型选 bool 还是 DateTime?
两种都常见,但行为和维护成本不同:
-
bool IsDeleted:简单直接,索引小,查询快(!x.IsDeleted可走索引),适合大多数场景;缺点是无法知道删于何时、谁删的 -
DateTime? DeletedAt:能记录删除时间,便于审计;但DeletedAt == null的判断在某些数据库(如 SQL Server)可能无法高效利用索引,且默认值需手动设为null - 如果后续要加逻辑(如“7天后自动物理删除”),
DateTime?更易扩展;若只是开关式软删,bool更轻量
无论选哪种,都要确保迁移脚本里该字段允许为空(DeletedAt)或有默认值(IsDeleted = false),否则新增实体插入失败。
软删除与 SaveChanges 如何联动
EF Core 不会自动把 Delete 操作转为更新,必须手动拦截。推荐在 SaveChanges 重写中处理:
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
foreach (var entry in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Deleted))
{
entry.State = EntityState.Modified;
entry.CurrentValues["IsDeleted"] = true;
}
return base.SaveChanges(acceptAllChangesOnSuccess);
}
其中 ISoftDelete 是你定义的接口(含 IsDeleted 属性)。关键点:
- 必须检查
entry.State == EntityState.Deleted,否则修改其他状态的实体会出错 - 用
entry.CurrentValues["IsDeleted"] = true而非entry.Entity.IsDeleted = true,避免触发属性 setter 里的业务逻辑(比如日志) - 若使用异步
SaveChangesAsync,需重写对应异步版本 - 物理删除仍应保留(比如管理员强制清理),可通过临时禁用跟踪或标记特殊上下文来区分
真正难的不是加过滤器,而是让团队所有人记住:所有 Delete 都不该直接调用 Remove,所有查询默认看不到已删数据——这两条纪律一旦松动,软删除就形同虚设。










