EF Core软删除必须用HasQueryFilter实现全局查询拦截,仅加IsDeleted字段无效;需在OnModelCreating中为实体配置,且仅影响查询操作,SaveChanges不受影响。

EFCore 软删除必须用 HasQueryFilter,不是加个字段就完事
软删除不是靠业务层手动写 WHERE IsDeleted = false,EF Core 里它本质是「全局查询拦截」。不配置 HasQueryFilter,哪怕模型里有 IsDeleted 字段,查询照样返回已“删”数据——这是最常踩的坑。
配置位置在 OnModelCreating 里,且只对继承自 DbContext 的实体生效:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<order>()
.HasQueryFilter(x => !x.IsDeleted);
}</order>
-
HasQueryFilter只影响查询(Find、ToList、First等),不影响SaveChanges - 若实体有多个软删除字段(如
IsDeleted+DeletedBy),过滤器里得全写上,否则可能漏筛 - 导航属性加载(
Include)也会被过滤,但注意:如果主表没被过滤(比如你临时禁用了),关联表仍按自己的过滤器走
全局筛选器要支持“查已删数据”,得用 IgnoreQueryFilters
管理后台常要查“已删除订单”,这时不能删掉过滤器重配,而是运行时绕过。EF Core 提供 IgnoreQueryFilters 方法,但它只作用于当前查询链:
var deletedOrders = context.Orders
.IgnoreQueryFilters()
.Where(x => x.IsDeleted)
.ToList();
-
IgnoreQueryFilters()必须放在Where前,放后面无效 - 它不解除嵌套查询的过滤(比如
Include(x => x.Items)中的Items仍受自己过滤器约束) - 别在仓储基类里默认调用它——会破坏软删除语义,除非你明确分“前台/后台”上下文
IsDeleted 字段类型和默认值直接影响迁移和性能
用 bool 最稳妥,但有人图省事用 int 或 string 存状态(如 0/1/"Y"/"N"),这会导致两个问题:迁移失败 + 查询无法内联。
- EF Core 6+ 对
HasQueryFilter中的表达式有编译限制,x.Status == 1可能报InvalidOperationException: The LINQ expression could not be translated - 数据库索引失效:如果字段是
int,即使加了WHERE Status = 0,优化器也可能不走索引(尤其 SQL Server) - 迁移脚本里
IsDeleted默认值必须设为false,否则老数据全被“过滤掉”,上线就丢数据
软删除 + 级联删除混用时,HasForeignKey 配置必须显式声明
比如 Order 和 OrderItem,你希望删 Order 时只软删,但 OrderItem 也得跟着软删。这时候不能依赖 EF 默认级联,必须手动配:
modelBuilder.Entity<orderitem>()
.HasOne(x => x.Order)
.WithMany(x => x.Items)
.HasForeignKey(x => x.OrderId)
.OnDelete(DeleteBehavior.ClientSetNull); // 关键:不能用 Cascade</orderitem>
-
DeleteBehavior.Cascade会触发物理删除,直接绕过软删逻辑 -
ClientSetNull或NoAction更安全,软删动作由业务代码或触发器控制 - 如果用
ChangeTracker手动标记子项为Modified并改IsDeleted,记得关掉自动检测变更(context.ChangeTracker.AutoDetectChangesEnabled = false),否则性能跳变
软删除真正难的不是加一行 HasQueryFilter,而是所有关联操作都得重新对齐语义——查、删、连查、导出、甚至备份还原,都得心里有数哪一层在过滤、哪一层被绕过了。











