主要参考
QueryFilter
QueryFilter 就是默认过滤, 非常适合用来做 Soft Delete
builder.HasQueryFilter(e => EF.Property<DateTimeOffset?>(e, "DateDeleted") == null);
设置这个以后, 一般的 query 语句就拿不到 deleted 的 row 了
如果想获取 deleted row 那么就需要通过 IgnoreQueryFilters 来 by pass 它.
blogs = db.Blogs .Include(b => b.Posts) .IgnoreQueryFilters() .ToList();
Interception
Interception 也适合用来做 Soft Delete 或者简单的 Audit Trail
但是还有一个更简单的做法是直接 override SaveChangesAsync 方法
去 DbContext class
public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) { foreach (var entry in ChangeTracker.Entries()) { if (entry.State == EntityState.Added) { } else if (entry.State == EntityState.Modified) { } else if (entry.State == EntityState.Deleted) { } var tableName = entry.Metadata.GetTableName(); foreach (var property in entry.Properties) { var isModified = property.IsModified; var originalValue = property.OriginalValue; var currentValue = property.CurrentValue; var metadata = property.Metadata; } } return base.SaveChangesAsync(cancellationToken); }
获取 Entry 资料, 然后修改 Entry 就可以操控最终 save 的结构了. (比如把 Deleted 换成 Modified)
创建一个 Interceptor (我这里用 SaveChangesInterceptor 举例)
public class SoftDeleteInterception : SaveChangesInterceptor { public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData, InterceptionResult<int> result, CancellationToken cancellationToken = default) { var entities = eventData.Context!.ChangeTracker.Entries(); return new ValueTask<InterceptionResult<int>>(result); } }
注: SavingChangesAsync 是 before SQL, SavedChangesAsync 是 after success SQL, FailedChangesAsync 是 after fail SQL.
在 DbContext class register 这个 interceptor
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.AddInterceptors(new SoftDeleteInterception());
Change State in SaveChanges
看注释, 有些逻辑和平时会不同,比如 set value 时, 不会 update IsModified (但 generate 出来的语句还是有的, 我想可能 EF 最后还会再对比一次吧...懒惰去研究了)
foreach (var entry in eventData.Context!.ChangeTracker.Entries()) { var entityType = entry.Entity.GetType(); if (entityType.FullName == "TestEFCore.Product") { entry.State = EntityState.Unchanged; // if change to modified then all properties will become IsModified typeof(Product).GetProperty("Name")!.SetValue(entry.Entity, "New Value"); // current value 会 update, but IsModified 依然是 false entry.Property("DateDeleted").CurrentValue = DateTimeOffset.Now; entry.Property("DateDeleted").IsModified = true; // will update entry.State to modified foreach (var p in entry.Properties) { var isModified = p.IsModified; } var state = entry.State; } }
Dependency Injection inside Interceptor
参考:
Ability to register IInterceptor without an IDbContextOptionsExtension
A better way of resolving EF Core interceptors with dependency injection