如何使用AutoMapper与EF Core 2更新对象集合?
我在尝试更新ProgramacionSemanal及其关联的ProgramacionSemanalDetalles集合时遇到了棘手的问题,保存更改时抛出了实体跟踪冲突的错误。以下是我的相关代码和错误信息:
实体模型
public class ProgramacionSemanal { public int ProgramacionSemanalId { get; set; } public int Semana { get; set; } public DateTime FechaInicio { get; set; } public DateTime FechaFin { get; set; } public ICollection<ProgramacionSemanalDetalle> ProgramacionSemanalDetalles { get; set; } } public class ProgramacionSemanalDetalle { [Key] public int ProgramacionSemanalDetalleId { get; set; } public int ProgramacionSemanalId { get; set; } [Column("ClienteId")] public Entidad Cliente { get; set; } public int UbicacionId { get; set; } public Ubicacion Ubicacion { get; set; } public int ProductoId { get; set; } public Producto Producto { get; set; } public int Lunes { get; set; } public int Martes { get; set; } public int Miercoles { get; set; } public int Jueves { get; set; } public int Viernes { get; set; } public int Sabado { get; set; } public int Domingo { get; set; } public AppUsuario Usuario { get; set; } }
API的PUT路由代码
[HttpPut("{programacionsemanalId}")] public async Task<IActionResult> Put(int programacionSemanalId, [FromBody] ProgramacionSemanalViewModel model) { try { var oldProgramacionSemanal = await _repository.GetProgramacionSemanalAsync(programacionSemanalId); if (oldProgramacionSemanal == null) return NotFound($"No se encontro la Programación Semanal."); _mapper.Map(model, oldProgramacionSemanal); var currentUser = await _userManager.FindByNameAsync(User.Identity.Name); foreach (var item in oldProgramacionSemanal.ProgramacionSemanalDetalles) { var producto = await _repository.GetProductoAsync(item.ProductoId); if (producto == null) return BadRequest(); item.Producto = producto; var ubicacion = await _repository.GetUbicacionAsync(item.UbicacionId); if (ubicacion == null) return BadRequest(); item.Ubicacion = ubicacion; item.Usuario = currentUser; } if (await _repository.SaveAllAsync()) { return Ok(_mapper.Map<ProgramacionSemanalViewModel>(oldProgramacionSemanal)); } } catch (Exception ex) { _logger.LogError($"Threw exception while updating: {ex}"); } return BadRequest("Couldn't update"); }
抛出的错误信息
System.InvalidOperationException: The instance of entity type 'ProgramacionSemanalDetalle' cannot be tracked because another instance with the key value '{ProgramacionSemanalDetalleId: 38}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap1.ThrowIdentityConflict(InternalEntityEntry entry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap1.Add(TKey key, InternalEntityEntry entry, Boolean updateDuplicate)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, Boolean acceptChanges, Nullable1 forceStateWhenUnknownKey) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode node, Boolean force) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode node, TState state, Func3 handleNode)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.AttachGraph(InternalEntityEntry rootEntry, EntityState entityState, Boolean forceStateWhenUnknownKey)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NavigationFixer.NavigationCollectionChanged(InternalEntityEntry entry, INavigation navigation, IEnumerable1 added, IEnumerable1 removed)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntryNotifier.NavigationCollectionChanged(InternalEntityEntry entry, INavigation navigation, IEnumerable1 added, IEnumerable1 removed)
at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.ChangeDetector.DetectNavig
问题分析与解决方案
这个错误的核心是EF Core的实体跟踪机制在起作用:当你从数据库加载oldProgramacionSemanal时,它关联的ProgramacionSemanalDetalles集合已经被DbContext跟踪了。而AutoMapper的默认映射行为可能会用ViewModel中的集合完全替换原集合,导致新的ProgramacionSemanalDetalle实例(和旧实例主键相同)被添加到上下文中,同时旧实例还在被跟踪,引发冲突。另外,你手动获取Producto和Ubicacion的操作也可能引入重复跟踪的实体。
下面是几个针对性的解决方案:
1. 配置AutoMapper保留原集合,仅更新集合内实体
默认AutoMapper会替换整个集合,我们需要修改映射配置,让它复用原有的集合实例,只更新其中的实体属性:
// 在你的AutoMapperProfile配置类中添加 CreateMap<ProgramacionSemanalViewModel, ProgramacionSemanal>() // 告诉AutoMapper使用目标对象的现有集合,而不是替换它 .ForMember(dest => dest.ProgramacionSemanalDetalles, opt => opt.UseDestinationValue()) // 同时配置子实体的映射规则 .ForMember(dest => dest.ProgramacionSemanalDetalles, opt => opt.MapFrom(src => src.ProgramacionSemanalDetalles)); // 还要配置子实体的ViewModel到实体的映射 CreateMap<ProgramacionSemanalDetalleViewModel, ProgramacionSemanalDetalle>();
这样AutoMapper会遍历原集合中的每个实体,用ViewModel对应的数据更新它们的属性,而不会创建新的实体实例,从根源上避免跟踪冲突。
2. 避免重复获取已跟踪的关联实体
当你加载oldProgramacionSemanal时,如果你的GetProgramacionSemanalAsync方法已经包含了Include(x => x.ProgramacionSemanalDetalles)(或者延迟加载已启用),那么集合中的ProgramacionSemanalDetalle已经被上下文跟踪,你不需要再手动获取Producto和Ubicacion——EF Core会自动维护这些关联的跟踪状态。你只需要检查ID是否存在,然后直接设置Usuario即可:
foreach (var item in oldProgramacionSemanal.ProgramacionSemanalDetalles) { // 只检查实体是否存在,不获取实体实例(避免跟踪新实例) if (!await _repository.ExistsProductoAsync(item.ProductoId)) return BadRequest($"Producto con ID {item.ProductoId} no existe"); if (!await _repository.ExistsUbicacionAsync(item.UbicacionId)) return BadRequest($"Ubicación con ID {item.UbicacionId} no existe"); item.Usuario = currentUser; }
如果你的仓储没有Exists方法,可以用Any查询:
if (!await _context.Productos.AnyAsync(p => p.ProductoId == item.ProductoId)) return BadRequest(...);
3. 对查询的关联实体使用AsNoTracking
如果你确实需要获取Producto和Ubicacion,可以在查询时加上AsNoTracking(),这样DbContext不会跟踪这些实例,也就不会和已有的跟踪实例冲突:
var producto = await _repository.GetProductoAsync(item.ProductoId).AsNoTracking(); var ubicacion = await _repository.GetUbicacionAsync(item.UbicacionId).AsNoTracking();
注意:这种方式仅适用于你不需要修改Producto或Ubicacion实体的场景,如果你只是要设置关联,这完全可行。
4. 手动处理实体跟踪状态
如果以上方法都不适用,你可以手动检查上下文是否已经在跟踪某个实体,优先使用已跟踪的实例:
var producto = await _repository.GetProductoAsync(item.ProductoId); if (producto == null) return BadRequest(); // 检查本地缓存中是否有已跟踪的Producto实例 var trackedProducto = _context.Productos.Local.FirstOrDefault(p => p.ProductoId == producto.ProductoId); if (trackedProducto != null) item.Producto = trackedProducto; // 使用已跟踪的实例 else item.Producto = producto; // 附加新实例 // Ubicacion的处理逻辑同理
内容的提问来源于stack exchange,提问作者Alberto López

