EF Core 通过完全替换断开集合导航属性更新

时间:2021-07-12 16:29:09

标签: c# entity-framework asp.net-core entity-framework-core

使用 EF Core 5.0。我有一个 SPA 页面,它加载一个 Group 实体及其来自 API 的 Employee 实体集合:

var groupToUpdate = await context.Groups 
                                 .Include(g => g.Employees)
                                 .FirstOrDefaultAsync(...);

//Used for UI, list of additional employees for selective adding
var employeeList = await context.Employees.
                                .Where(...)
                                .ToListAsync();

然后用户通过 Javascript UI 修改 groupToUpdate 实体,包括一些非导航属性,例如名称/注释。

在同一屏幕上,用户将一些员工添加到组中,从组中删除一些员工,并保留组中的一些现有员工。所有员工都是数据库中具有现有主键的现有实体。到目前为止所做的所有更改都只是针对内存中断开连接的实体。

当用户点击保存时,groupToUpdate 实体被发送到我的后端代码。请注意,我们没有跟踪添加/删除/留下哪些员工,我们只是想让这个 groupToUpdate 完全覆盖旧实体,特别是用新实体替换旧的 Employees 集合.

为了实现这一点,后端代码首先从数据库中再次加载组以开始在上下文中跟踪它。然后我尝试更新实体,包括用新集合替换旧集合:

public async Task UpdateGroupAsync(Group groupToUpdate)
{
    var groupFromDb = await context.Groups
                                   .Include(g => g.Employees)
                                   .FirstOrDefaultAsync(...);
    
    // Update non-navigation properties such as groupFromDb.Note = groupToUpdate.Note...

    groupFromDb.Employees = groupToUpdate.Employees;

    await context.SaveChangesAsync();
}

现在,如果对 Employees 集合的更改是完全替换(删除所有旧的,添加所有新的),则此方法成功。但是只要有一些现有的 Employees 被搁置,EF 核心就会抛出异常:

<块引用>

无法跟踪实体类型 'Employee' 的实例,因为另一个具有键值 ... 的实例已被跟踪

因此,EF Core 似乎尝试同时跟踪 EmployeegroupFromDb 从数据库中新加载的 groupToUpdate 实体,即使后者仅作为来自断开连接状态的参数。

我的问题是如何以最少的复杂性处理这种更新?是否有必要手动跟踪添加/删除的实体并添加/删除它们而不是尝试替换整个集合?

1 个答案:

答案 0 :(得分:1)

您必须指示 ChangeTracker 更新导航集合需要哪些操作。只是替换集合不是正确的方法。

这是有助于自动执行此操作的扩展程序:

context.MergeCollections(groupFromDb.Employees, groupToUpdate.Employees, x => x.Id);

实施:

public static void MergeCollections<T, TKey>(this DbContext context, ICollection<T> currentItems, ICollection<T> newItems, Func<T, TKey> keyFunc) 
    where T : class
{
    List<T> toRemove = null;
    foreach (var item in currentItems)
    {
        var currentKey = keyFunc(item);
        var found = newItems.FirstOrDefault(x => currentKey.Equals(keyFunc(x)));
        if (found == null)
        {
            toRemove ??= new List<T>();
            toRemove.Add(item);
        }
        else
        {
            if (!ReferenceEquals(found, item))
                context.Entry(item).CurrentValues.SetValues(found);
        }
    }

    if (toRemove != null)
    {
        foreach (var item in toRemove)
        {
            currentItems.Remove(item);
            // If the item should be deleted from Db: context.Set<T>().Remove(item);
        }
    }

    foreach (var newItem in newItems)
    {
        var newKey = keyFunc(newItem);
        var found = currentItems.FirstOrDefault(x => newKey.Equals(keyFunc(x)));
        if (found == null)
        {
            currentItems.Add(newItem);
        }
    }
}
相关问题