现在是 2021 年,我想到添加一个与当前版本的 EF Core 相关的更现代、标准、内置的解决方案。
With 全局查询过滤器 https://learn.microsoft.com/en-us/ef/core/querying/filters您可以确保某些过滤器始终应用于某些实体。您可以通过接口定义软删除属性,这有助于以编程方式将过滤器添加到所有相关实体。看:
...
public interface ISoftDeletable
{
public string DeletedBy { get; }
public DateTime? DeletedAt { get; }
}
...
// Call it from DbContext.OnModelCreating()
private static void ConfigureSoftDeleteFilter(ModelBuilder builder)
{
foreach (var softDeletableTypeBuilder in builder.Model.GetEntityTypes()
.Where(x => typeof(ISoftDeletable).IsAssignableFrom(x.ClrType)))
{
var parameter = Expression.Parameter(softDeletableTypeBuilder.ClrType, "p");
softDeletableTypeBuilder.SetQueryFilter(
Expression.Lambda(
Expression.Equal(
Expression.Property(parameter, nameof(ISoftDeletable.DeletedAt)),
Expression.Constant(null)),
parameter)
);
}
}
然后,为了确保在删除期间使用此标志而不是硬删除(替代例如存储库设置标志而不是删除实体):
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
{
foreach (var entry in ChangeTracker.Entries<ISoftDeletable>())
{
switch (entry.State)
{
case EntityState.Deleted:
// Override removal. Unchanged is better than Modified, because the latter flags ALL properties for update.
// With Unchanged, the change tracker will pick up on the freshly changed properties and save them.
entry.State = EntityState.Unchanged;
entry.Property(nameof(ISoftDeletable.DeletedBy)).CurrentValue = _currentUser.UserId;
entry.Property(nameof(ISoftDeletable.DeletedAt)).CurrentValue = _dateTime.Now;
break;
}
}
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
注意事项 1:级联删除时机
一个关键的方面是考虑相关实体的级联删除,要么禁用级联删除,要么了解和控制 EF Core 的级联删除时序行为。默认值CascadeDeleteTiming
设置是CascadeTiming.Immediate
,这会导致 EF Core 立即将“已删除”实体的所有导航属性标记为EntityState.Deleted
,并恢复EntityState.Deleted
仅在根实体上的状态不会在导航属性上恢复它。因此,如果您的导航属性不使用软删除,并且您希望避免它们被删除,则也必须处理它们的更改跟踪器状态(而不是仅仅处理它,例如ISoftDeletable
实体),或更改CascadeDeleteTiming
设置如下图。
对于拥有的类型 https://learn.microsoft.com/en-us/ef/core/modeling/owned-entities用于软删除实体。使用默认删除级联计时,EF Core 还将这些拥有的类型标记为“已删除”,如果它们设置为必需/不可为空,则在尝试保存软删除实体时将遇到 SQL 更新失败。
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
ChangeTracker.CascadeDeleteTiming = CascadeTiming.OnSaveChanges;
}
注意事项 2:对其他根实体的影响
如果您以这种方式定义全局查询过滤器,EF Core 将尽力隐藏引用软删除实体的所有其他实体。
例如,如果您软删除了Partner
实体,并且你有Order
其中每个实体都通过(必需)导航属性引用合作伙伴,然后,当您检索订单列表并包含合作伙伴时,所有引用软删除的订单Partner
将从列表中消失。
此行为在文档页面底部 https://learn.microsoft.com/en-us/ef/core/querying/filters#accessing-entity-with-query-filter-using-required-navigation.
遗憾的是,从 EF Core 5 开始,全局查询过滤器不提供将其限制为根实体或仅禁用其中一个过滤器的选项。唯一可用的选项是使用IgnoreQueryFilters()
方法,禁用所有过滤器。并且自从IgnoreQueryFilters()
方法需要一个IQueryable
并且还返回一个IQueryable
,您不能使用此方法透明地禁用 DbContext 类中公开的过滤器DbSet
.
不过,一个重要的细节是,只有当您Include()
查询时给定的导航属性。有一个有趣的解决方案,用于获取结果集,该结果集将查询过滤器应用于某些实体,但没有将它们应用于其他实体,这依赖于 EF 的一个鲜为人知的功能,关系修复。基本上,您加载一个列表EntityA
具有导航属性EntityB
(不包括EntityB
)。然后你单独加载列表EntityB
, using IgnoreQueryFilters()
。发生的情况是 EF 自动设置EntityB
导航属性打开EntityA
到加载的EntityB
实例。这样查询过滤器就被应用到EntityA
本身,但没有应用于EntityB
导航属性,这样你就可以看到EntityA
即使是软删除的EntityB
s. 请参阅另一个问题的这个答案 https://stackoverflow.com/a/63280491/2906385。 (当然这会对性能产生影响,并且您仍然无法将其封装在 DbContext 中。)