EF Core 7更新数据时出现SiteFrame跟踪冲突错误求助
场景与代码
使用.NET 7 + Entity Framework Core 7,数据库为PostgreSQL,上下文注册为Scoped,通过AutoMapper完成DTO到实体的映射。
更新方法代码
public async Task UpdateAsync(int id, SiteDto dto) { Site site = await _context.Sites.Where(x => x.Id == id) .Include(x => x.Network) .Include(x => x.Centre) .Include(x => x.SiteDayParts).ThenInclude(x => x.SiteFrames) .Include(x => x.Resolution) .SingleOrDefaultAsync(); _mapper.Map(dto, site); await _context.SaveChangesAsync(); }
DTO类定义
public class SiteDto { public string Name { get; set; } public string Address { get; set; } public MediaFormat Formats { get; set; } public int ResolutionId { get; set; } public List<SiteDayPartDto> SiteDayParts { get; set; } } public class SiteDayPartDto { public int Id { get; set; } public TimeSpan StartTime { get; set; } public TimeSpan EndTime { get; set; } public int Duration { get; set; } public List<SiteFrameDto> SiteFrames { get; set; } } public class SiteFrameDto { public int Id { get; set; } public int FrameId { get; set; } }
AutoMapper映射配置
CreateMap<SiteDto, Site>(); CreateMap<SiteDayPartDto, SiteDayPart>() .ForMember(dest => dest.AdLength, opt => opt.MapFrom(x => x.Duration)); CreateMap<SiteFrameDto, SiteFrame>();
错误信息
The instance of entity type 'SiteFrame' cannot be tracked because another instance with the key value '{Id: 1}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
注:相同逻辑在Entity Framework 6中可正常运行。
问题原因
EF Core与EF6的实体跟踪机制存在差异:当通过Include加载SiteFrames后,这些实体已被上下文跟踪。而AutoMapper默认会直接替换实体集合(如SiteFrames),或在映射时创建新的SiteFrame实例(即使DTO包含已存在的Id),导致同一个主键的实体出现两个被跟踪的实例,触发冲突。
解决方案
方案1:手动处理关联集合的增删改
放弃AutoMapper自动映射集合,手动遍历DTO集合,匹配已有实体进行属性更新,同时处理新增和删除:
public async Task UpdateAsync(int id, SiteDto dto) { Site site = await _context.Sites.Where(x => x.Id == id) .Include(x => x.Network) .Include(x => x.Centre) .Include(x => x.SiteDayParts).ThenInclude(x => x.SiteFrames) .Include(x => x.Resolution) .SingleOrDefaultAsync(); // 映射Site的基础属性 _mapper.Map(dto, site); // 处理SiteDayParts foreach (var dayPartDto in dto.SiteDayParts) { var existingDayPart = site.SiteDayParts.FirstOrDefault(dp => dp.Id == dayPartDto.Id); if (existingDayPart != null) { // 映射SiteDayPart的基础属性 _mapper.Map(dayPartDto, existingDayPart); // 移除不在DTO中的SiteFrame var framesToRemove = existingDayPart.SiteFrames .Where(f => !dayPartDto.SiteFrames.Any(dtoF => dtoF.Id == f.Id)) .ToList(); foreach (var frame in framesToRemove) { existingDayPart.SiteFrames.Remove(frame); _context.SiteFrames.Remove(frame); } // 更新或新增SiteFrame foreach (var frameDto in dayPartDto.SiteFrames) { var existingFrame = existingDayPart.SiteFrames.FirstOrDefault(f => f.Id == frameDto.Id); if (existingFrame != null) { _mapper.Map(frameDto, existingFrame); } else { var newFrame = _mapper.Map<SiteFrame>(frameDto); existingDayPart.SiteFrames.Add(newFrame); } } } else { // 新增SiteDayPart及其关联SiteFrame var newDayPart = _mapper.Map<SiteDayPart>(dayPartDto); site.SiteDayParts.Add(newDayPart); } } // 移除不在DTO中的SiteDayPart var dayPartsToRemove = site.SiteDayParts .Where(dp => !dto.SiteDayParts.Any(dtoDp => dtoDp.Id == dp.Id)) .ToList(); foreach (var dayPart in dayPartsToRemove) { site.SiteDayParts.Remove(dayPart); _context.SiteDayParts.Remove(dayPart); } await _context.SaveChangesAsync(); }
方案2:配置AutoMapper不替换集合,映射现有元素
修改AutoMapper配置,针对集合类型,指定使用已跟踪的实体进行映射,而非创建新实例:
CreateMap<SiteDto, Site>() .ForMember(dest => dest.SiteDayParts, opt => opt.MapFrom((src, dest) => dest?.SiteDayParts != null ? MapSiteDayParts(src.SiteDayParts, dest.SiteDayParts) : src.SiteDayParts.Select(dto => _mapper.Map<SiteDayPart>(dto)) )); CreateMap<SiteDayPartDto, SiteDayPart>() .ForMember(dest => dest.AdLength, opt => opt.MapFrom(x => x.Duration)) .ForMember(dest => dest.SiteFrames, opt => opt.MapFrom((src, dest) => dest?.SiteFrames != null ? MapSiteFrames(src.SiteFrames, dest.SiteFrames) : src.SiteFrames.Select(dto => _mapper.Map<SiteFrame>(dto)) )); CreateMap<SiteFrameDto, SiteFrame>(); // 辅助方法:映射SiteDayPart集合,复用现有实体 private List<SiteDayPart> MapSiteDayParts(List<SiteDayPartDto> dtos, List<SiteDayPart> existingParts) { foreach (var dto in dtos) { var existingPart = existingParts.FirstOrDefault(p => p.Id == dto.Id); if (existingPart != null) { _mapper.Map(dto, existingPart); } else { existingParts.Add(_mapper.Map<SiteDayPart>(dto)); } } // 移除不在DTO中的元素 var partsToRemove = existingParts.Where(p => !dtos.Any(dto => dto.Id == p.Id)).ToList(); foreach (var part in partsToRemove) { existingParts.Remove(part); } return existingParts; } // 辅助方法:映射SiteFrame集合,复用现有实体 private List<SiteFrame> MapSiteFrames(List<SiteFrameDto> dtos, List<SiteFrame> existingFrames) { foreach (var dto in dtos) { var existingFrame = existingFrames.FirstOrDefault(f => f.Id == dto.Id); if (existingFrame != null) { _mapper.Map(dto, existingFrame); } else { existingFrames.Add(_mapper.Map<SiteFrame>(dto)); } } // 移除不在DTO中的元素 var framesToRemove = existingFrames.Where(f => !dtos.Any(dto => dto.Id == f.Id)).ToList(); foreach (var frame in framesToRemove) { existingFrames.Remove(frame); } return existingFrames; }
方案3:关闭查询跟踪,手动管理实体状态
如果不需要保留原查询的跟踪,可以用AsNoTracking()加载实体,之后手动设置实体状态为Modified,并处理关联实体的状态:
public async Task UpdateAsync(int id, SiteDto dto) { Site site = await _context.Sites.Where(x => x.Id == id) .Include(x => x.Network) .Include(x => x.Centre) .Include(x => x.SiteDayParts).ThenInclude(x => x.SiteFrames) .Include(x => x.Resolution) .AsNoTracking() // 关闭跟踪 .SingleOrDefaultAsync(); _mapper.Map(dto, site); // 手动设置Site状态为修改 _context.Sites.Update(site); // 手动处理关联实体状态(如需) foreach (var dayPart in site.SiteDayParts) { if (dayPart.Id > 0) { _context.SiteDayParts.Update(dayPart); foreach (var frame in dayPart.SiteFrames) { if (frame.Id > 0) { _context.SiteFrames.Update(frame); } else { _context.SiteFrames.Add(frame); } } } else { _context.SiteDayParts.Add(dayPart); } } await _context.SaveChangesAsync(); }
注:此方案需注意关联实体的状态管理,避免重复更新或遗漏新增/删除操作。
内容的提问来源于stack exchange,提问作者Semyon Falshin

