C# 不使用AsNoTracking更新嵌套列表

C# 不使用AsNoTracking更新嵌套列表,c#,entity-framework,entity-framework-core,C#,Entity Framework,Entity Framework Core,简化模型: Profile {Guid Id, string Name, List<Property> Properties} Property {Guid Id, string Name, List<Type> Types} Type {Guid Id, string Key, string Value} 在更新服务中: public async Task<Response> Update([FromBody] Profile profile) { var

简化模型:

Profile {Guid Id, string Name, List<Property> Properties}
Property {Guid Id, string Name, List<Type> Types}
Type {Guid Id, string Key, string Value}
在更新服务中:

public async Task<Response> Update([FromBody] Profile profile)
{
var entity = await _context.Profiles
    .Include(x => x.Properties)
    .ThenInclude(x => x.Types)
    .FirstOrDefaultAsync(x => x.Id == profile.Id);

foreach (var prop in profile.Properties)
{
    var existingProp = entity.Properties.SingleOrDefault(a => a.Id == prop.Id);
    //Update
    if (existingProp != null)
    {
        var entry = _context.Entry(existingProp);
        entry.State = EntityState.Modified;
        existingProp.ChargeFrom(prop);//maps the new values to the db entity
        _context.SaveChanges();
    }
}
}
问题解决了,但我不知道为什么会抛出这个异常,其他一些线程提到DbContext可能被另一个进程使用,并且可能注册为Singleton,但在我的例子中,它注册为scoped

我将类型实体标记为Notracking:
。然后包括(x=>x.Types)。AsNoTracking()
问题解决了,但我不知道为什么会抛出这个异常

错误的原因将是因为此行:

existingProp.ChargeFrom(prop);//maps the new values to the db entity
。。。将尝试将未跟踪的类型从
prop
复制到
existingProp
。使用
AsNoTracking
将删除异常,但很可能会导致
SaveChanges
上的数据重复,其中
Type
将使用标识键或重复行异常进行设置。如果您没有收到任何异常,我将检查Types集合,看看是否有重复的行出现在那里

将数据从未跟踪的实体复制到跟踪的实体时,您需要确保只复制值,而不复制引用。复制一个未跟踪的引用,默认情况下EF会将其视为一个新实体。即使强制将其状态切换到
Modified
,DbContext也可能已经在跟踪具有该ID的实体

如果Property.Types是引用的集合,例如与查找的关联,并且这些引用可能会更改添加和删除关联的位置,则要应用更改,需要从数据库加载关联的类型,然后使用该类型删除不再有效的关联,并添加当前未关联的关联

例如:给定一个类型为(Type1)和(Type2)的属性(PropertyA),如果我们将其编辑为具有(Type1)和(Type3),则需要从DbContext(跟踪)中获取Type1和Type3,然后与跟踪的属性进行比较,以确定删除Type2并添加Type3

var entity = await _context.Profiles
    .Include(x => x.Properties)
    .ThenInclude(x => x.Types)
    .SingleAsync(x => x.Id == profile.Id);

// Get the IDs for all Types we want to associate... In the above example this would
// ask for Type1 and Type3 if only the one property. We get a Distinct list because
// multiple properties might reference the same TypeId(s).
var existingTypeIds = profile.Properties
    .SelectMany(x => x.Types.Select(t => t.Id))
    .Distinct()
    .ToList();

// Load references to all Types that will be needed. Where associating new types, these will be referenced.
var existingTypes = _context.Types
    .Where(x => existingTypeIds.Contains(x.Id))
    .ToList();

foreach (var prop in profile.Properties)
{   
    existingProp = entity.Properties.SingleOrDefault(x => x.Id == prop.Id);
    if (existingProp == null)
        continue;

    var updatedTypeIds = prop.Types.Select(x => x.Id).ToList();
    var existingTypeIds = existingProp.Types.Select(x => x.Id).ToList();

    var addedTypeIds = updatedTypeIds.Except(existingTypeIds).ToList();
    var removedTypeIds = existingTypeIds.Except(updatedTypeIds).ToList();

    var addedTypes = existingTypes
        .Where(x => addedTypeIds.Contains(x.Id))
        .ToList();
    var removedTypes = existingProp.Types
        .Where(x => removedTypeIds.Contains(x.Id))
        .ToList();

    foreach(var removedType in removedTypes)
        existingProp.Types.Remove(removedType);

    foreach(var addedType in addedTypes)
        existingProp.Types.Add(addedType);

}
如果该类型是包含可更新属性的子行,则应在更新数据和现有数据状态之间复制这些值。这增加了大量的工作,尽管像AutoMapper这样的工具可以配置来提供帮助。您仍然需要管理可以添加、删除类型或更改内容的情况。这也适用于属性,因为您的示例只处理属性被更新的情况,而不是潜在地添加或删除的情况

最终,尝试将更新场景组织为尽可能原子化是有益的,以避免对实体、属性和类型的整个对象图进行更改的更新,而不是仅对实体值进行一次更新,对属性值进行一次更新,对单一类型更新进行一次更新。这也适用于添加属性、添加类型、删除属性和删除类型。虽然看起来像是更多的代码来分解这样的操作,但它使它们非常简单和直接,而不是一个大而复杂的方法试图比较之前和之后,以确定要添加、删除和更新的内容。错误隐藏在复杂的代码中,而不是简单的方法中。:)

编辑对象图时,还应避免多次调用
SaveChanges
。它不应该在属性的循环中调用,而应该在循环完成时执行一次。这样做的原因是,其中一个属性上的异常会导致持久化的数据状态不完整/无效。如果要保存的对象中有4个属性,而第3个属性因任何原因出现异常而失败,则前2个属性将被更新,最后两个属性不会持久化。通常,在更新操作中,更新应该遵循“要么全有,要么全无”的持久性方法


希望这有助于解释你所看到的行为,并给你一些考虑前进的东西。

摆脱无用的代码>条目变量和<代码>条目。状态< /代码>修改和问题解决。code>existingProp已经被EF跟踪,您不必手动添加它来跟踪
Value
属性在没有它的情况下不会更新,或者父对象(
entity
)应该被
修改
,感谢您的完整回答,实际上我不希望如此复杂!我要求更新,因为我有问题。我可以用完整代码(属性(添加/删除/更新)、类型(添加/删除/更新))更新您的答案吗?
.ThenInclude(x => x.Types).AsNoTracking()
existingProp.ChargeFrom(prop);//maps the new values to the db entity
var entity = await _context.Profiles
    .Include(x => x.Properties)
    .ThenInclude(x => x.Types)
    .SingleAsync(x => x.Id == profile.Id);

// Get the IDs for all Types we want to associate... In the above example this would
// ask for Type1 and Type3 if only the one property. We get a Distinct list because
// multiple properties might reference the same TypeId(s).
var existingTypeIds = profile.Properties
    .SelectMany(x => x.Types.Select(t => t.Id))
    .Distinct()
    .ToList();

// Load references to all Types that will be needed. Where associating new types, these will be referenced.
var existingTypes = _context.Types
    .Where(x => existingTypeIds.Contains(x.Id))
    .ToList();

foreach (var prop in profile.Properties)
{   
    existingProp = entity.Properties.SingleOrDefault(x => x.Id == prop.Id);
    if (existingProp == null)
        continue;

    var updatedTypeIds = prop.Types.Select(x => x.Id).ToList();
    var existingTypeIds = existingProp.Types.Select(x => x.Id).ToList();

    var addedTypeIds = updatedTypeIds.Except(existingTypeIds).ToList();
    var removedTypeIds = existingTypeIds.Except(updatedTypeIds).ToList();

    var addedTypes = existingTypes
        .Where(x => addedTypeIds.Contains(x.Id))
        .ToList();
    var removedTypes = existingProp.Types
        .Where(x => removedTypeIds.Contains(x.Id))
        .ToList();

    foreach(var removedType in removedTypes)
        existingProp.Types.Remove(removedType);

    foreach(var addedType in addedTypes)
        existingProp.Types.Add(addedType);

}