从EFCore延迟加载的上下文中删除导航集合实体-导航集合不会立即更新



由于遗留原因,我们在EF Core中使用了注入式惰性加载器。

在下面的代码中,Context.Add(child)导致延迟加载的导航集合被更新,但Context.Remove(child)没有。必须先调用SaveChanges()。这是故意的吗?

现在我知道我可以调用Parent.Children.Remove(child),这是非常好的,但我们的解决方案需要调用Context.Remove(child)。我不喜欢在不理解为什么需要SaveChanges()的情况下就把它扔进去。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace EFDemo
{
public class Parent
{
private Action<object, string> _lazyLoader;
public Parent() { }
public Parent(Action<object, string> lazyLoader)
{
_lazyLoader = lazyLoader;
}

public int Id { get; set; }
public string Name { get; set; }
private List<Child> _children;

public List<Child> Children {
get
{
_lazyLoader?.Invoke(this, "Children");
_children ??= new List<Child>();
return _children;
} 
set => _children = value; 
}
}
public class Child
{
public int Id { get; set; }
public string Name { get; set; }
public int ParentId { get; set; }
public Parent Parent { get; set; }
}

public class EFDemoContext : DbContext
{
public DbSet<Parent> Parents { get; set; }
public DbSet<Child> Children { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
var connectionString =
"Server=tcp:localhost,1433;Initial Catalog=EFDemo;Persist Security Info=False;User ID=xxxxx;Password=xxx@xxxx;Encrypt=False;Max Pool Size=500;Pooling=True;";

optionsBuilder.UseSqlServer(connectionString)
.LogTo(Console.WriteLine, LogLevel.Information)
.EnableDetailedErrors()
.EnableSensitiveDataLogging();
base.OnConfiguring(optionsBuilder);
}
}
class Program
{
static void Main(string[] args)
{
var context = new EFDemoContext();
context.Database.EnsureDeleted();
context.Database.Migrate();
var p = new Parent()
{
Name = "Alice",
Children = new List<Child>
{
new() { Name = "Bob" },
new() { Name = "Charlie" },
new() { Name = "Doris" },
}
};
context.Add(p);
context.SaveChanges();
Debug.Assert(p.Children.Count == 3, "Initial children not found");
context.Add(new Child() { Name = "Elizabeth", Parent = p });
Debug.Assert(p.Children.Count == 4, "New child not found");
context.Remove(p.Children.First());
//context.SaveChanges(); // we need to save in order to succeed
Debug.Assert(p.Children.Count == 3, "Child not removed");
}
}
}

——后续——

这个问题准确地解决了这个问题。有一些不一致,我认为在如何修复发生的删除,但很明显,内部的EFCore修复这个打破了太多的东西。

这种EF Core行为与延迟加载无关,它是由上下文更改跟踪器的许多操作执行的所谓的导航修复引起的。

虽然看起来与Add不一致,但实际上它们(以及其他更改跟踪相关的操作)都试图确保跟踪的具有以下不变量实体:

  1. 被跟踪的Parent parent:对于parent.Children中每个被跟踪的Child child,则child.Parent == parent

  2. 跟踪Child child:如果child.Parent != null,那么child.Parent.Childen.Contains(child)

当你用child.Parent != null调用Child childAdd(或Attach,Update)时,由于不变量#2,EF Core会将它添加到parent.Children集合中,如果它还不存在的话。

然而,当你调用RemoveChild childchild.Parent != null)时,实体被标记为删除(State = EntityState.Deleted),但Parent属性没有被取消,因此由于上述规则,它不能从child.Parent.Children集合中删除。不仅如此,事实上,如果它不存在,它将被添加到该集合中,可以通过以下代码看到

var child = new Child { Id = 1, Parent = new Parent { Id = 1 } };
Debug.Assert(child.Parent.Children.Count == 0);
context.Remove(child);
Debug.Assert(child.Parent != null && child.Parent.Children.Contains(child));

现在的问题可能是为什么Parent没有被取消。我猜是因为Deleted状态被认为是临时的(稍后您可能会决定将其设置为其他东西,从而"取消删除")。"deleted"实体).

所以他们所做的就是等待你"确认";实体状态/操作,发生在SaveChanges之后,更具体地说,是在成功地在数据库中应用修改后的ChangeTracker.AcceptAllChanges()调用。这个调用将Deleted临时状态转换为Detached持久状态。由于Detached意味着实体将不再被跟踪,它们执行最终的导航修复,在这种情况下,从父集合中删除子集合并将父集合清空。


以上解释了Remove行为背后的基本原理。如果您不喜欢它,并希望立即从父集合中删除子集合,该怎么办?一种方法是用你自己的EF Core服务替换负责这个的EF Core服务——在本例中,INavigationFixer接口与NavigationFixer类中的默认实现,但两者都没有文档记录,并且被认为是"内部api"的一部分;(即使它们在c# POV中是公共的)——这对我来说不是问题,但对很多人来说却是。因此,另一种选择是挂钩到上下文更改跟踪事件并执行所需的操作。例如,将以下内容添加到派生上下文类


void Initialize()
{
ChangeTracker.Tracked += OnEntityTracked;
ChangeTracker.StateChanged += OnEntityStateChanged;
}
void OnEntityStateChanged(object sender, EntityStateChangedEventArgs e)
{
if (e.NewState == EntityState.Deleted)
OnEntityDeleted(e.Entry);
}
void OnEntityTracked(object sender, EntityTrackedEventArgs e)
{
if (!e.FromQuery && e.Entry.State == EntityState.Deleted)
OnEntityDeleted(e.Entry);
}
void OnEntityDeleted(EntityEntry entry)
{
foreach (var refEntry in entry.References)
{
var refEntity = refEntry.CurrentValue;
if (refEntity != null)
{
var inverseNavigation = refEntry.Metadata.Inverse;
if (inverseNavigation != null && inverseNavigation.IsCollection)
{
var collection = inverseNavigation.GetCollectionAccessor();
collection.Remove(refEntity, entry.Entity);
}
}
}
}

和从构造函数调用Initialize