像往常一样,将此问题发布到StackOverflow帮助我解决了问题。代码最初与上述问题中的代码不同,但我在编写问题时正在修复代码。
在写这个问题之前,我花了将近一天的时间试图找出问题所在,因此我尝试了不同的方法,例如重新创建实体实例并手动附加它们,将某些实体标记为未更改/修改,使用AsNoTracking甚至完全禁用所有实体的自动更改跟踪,并将所有实体标记为手动添加或修改。
结果表明,导致此问题的代码位于该子存储库的一个私有方法中,我忽略了该方法,因为我认为它与此无关。如果我没有忘记从中删除一些手动更改跟踪代码的话,这真的没有什么关系,因为这些代码基本上是在摆弄EF的自动更改跟踪程序,导致其行为不端。
但是,多亏了StackOverflow,问题才得以解决。当你和某人谈论这个问题时,你需要自己重新分析它,能够解释它的所有细节,以便与你交谈的人(在本例中,是SO社区)理解它。在重新分析时,您会注意到所有导致bits的小问题,这样就更容易诊断问题。
总之,如果有人因为标题而被这个问题吸引,通过谷歌搜索或w/e,这里有一些要点:
-
如果要在多个级别上更新实体,请始终调用
.Include
获取现有实体时包含所有相关导航属性。这将使它们全部加载到更改跟踪器中,您无需手动附加/标记。更新完成后,调用SaveChanges将正确保存所有更改。
-
当需要更新子实体时,不要对顶级实体使用AutoMapper,尤其是在更新子实体时必须实现一些附加逻辑的情况下。
-
永远不要像我在将Id设置为-1时尝试的那样更新主键,或者像我在控制器更新方法的这一行尝试的那样:
// The mapper will also update child.RelatedEntity.Id
Mapper.Map(child, oldChild);
-
如果需要处理已删除的项目,最好检测它们并将其存储在单独的列表中,然后手动为每个项目调用repository delete方法,其中repository delete方法将包含有关相关实体的一些最终附加逻辑。
-
如果需要更改相关实体的主键,则需要首先从关系中删除该相关实体,然后使用更新的键添加一个新实体。
以下是已更新的控制器操作,省略了空值和安全检查:
public async Task<OutputDto> Update(InputDto input)
{
// First get a real entity by Id from the repository
// This repository method returns:
// Context.Masters
// .Include(x => x.SuperMaster)
// .Include(x => x.Children)
// .ThenInclude(x => x.RelatedEntity)
// .FirstOrDefault(x => x.Id == id)
Master entity = await _masterRepository.Get(input.Id);
// Update the master entity properties manually
entity.SomeProperty = "Updated value";
// Prepare a list for any children with modified RelatedEntity
var changedChildren = new List<Child>();
foreach (var child in input.Children)
{
// Check to see if this is a new child item
if (entity.Children.All(x => x.Id != child.Id))
{
// Map the DTO to child entity and add it to the collection
entity.Children.Add(Mapper.Map<Child>(child));
continue;
}
// Check to see if this is an existing child item
var existingChild = entity.Children.FirstOrDefault(x => x.Id == child.Id);
if (existingChild == null)
{
continue;
}
// Check to see if the related entity was changed
if (existingChild.RelatedEntity.Id != child.RelatedEntity.Id)
{
// It was changed, add it to changedChildren list
changedChildren.Add(existingChild);
continue;
}
// It's safe to use AutoMapper to map the child entity and avoid updating properties manually,
// provided that it doesn't have child-items of their own
Mapper.Map(child, existingChild);
}
// Find which of the child entities should be deleted
// entity.IsTransient() is an extension method which returns true if the entity has just been added
foreach (var child in entity.Children.Where(x => !x.IsTransient()).ToList())
{
if (input.Children.Any(x => x.Id == child.Id))
{
continue;
}
// We don't have this entity in the list sent by the client.
// That means we should delete it
await _childRepository.DeleteAsync(child);
entity.Children.Remove(child);
}
// Parse children entities with modified related entities
foreach (var child in changedChildren)
{
var newChild = input.Children.FirstOrDefault(x => x.Id == child.Id);
// Delete the existing one
await _childRepository.DeleteAsync(child);
entity.Children.Remove(child);
// Add the new one
// It's OK to change the primary key here, as this one is a DTO, not a tracked entity,
// and besides, if the keys are autogenerated by the database, we can't have anything but 0 for a new entity
newChild.Id = 0;
entity.Djelovi.Add(Mapper.Map<Child>(newChild));
}
// And finally, call the repository update and return the result mapped to DTO
entity = await _repository.UpdateAsync(entity);
return MapToEntityDto(entity);
}