使用AutoMapper映射嵌套列表时遭遇实体跟踪冲突问题
AutoMapper映射嵌套列表时的实体跟踪冲突问题
使用AutoMapper 13.0.1映射嵌套集合时出现以下错误:
AutoMapper, System.InvalidOperationException: The instance of entity type 'PurchaseReqCharge' cannot be tracked because another instance with the key value '{Id: 3}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
相关代码
实体类
public class PurchaseReq { ... public string PrNumber { get; set; } // primary key public List<PurchaseReqItem> PurchaseReqItems { get; init; } = []; public List<PurchaseReqCharge> PurchaseReqCharges { get; init; } = []; } public class PurchaseReqItem { ... public int Id { get; init; } // primary key public string PrNumber { get; set; } // foreign key public PurchaseReq PurchaseReq { get; init; } //navigation property } public class PurchaseReqCharge { ... public int Id { get; init; } // primary key public string PrNumber { get; set; } // foreign key public PurchaseReq PurchaseReq { get; init; } //navigation property }
DTO类
public class PurchaseReqDto { ... public string PrNumber { get; set; } public List<PurchaseReqItemDto> PurchaseReqItems { get; init; } = []; public List<PurchaseReqChargeDto> PurchaseReqCharges { get; init; } = []; } public class PurchaseReqItemDto { ... public int Id { get; init; } public string PrNumber { get; set; } public PurchaseReqDto PurchaseReq { get; init; } } public class PurchaseReqChargeDto { ... public int Id { get; init; } public string PrNumber { get; set; } public PurchaseReqDto PurchaseReq { get; init; } }
映射配置
public class PurchaseReqProfile : Profile { public PurchaseReqProfile() { CreateMap<PurchaseReq, PurchaseReqDto>(); CreateMap<PurchaseReqDto, PurchaseReq>() .ForMember(d => d.PurchaseReqItems, o => o.MapFrom(src => src.PurchaseReqItems)) .ForMember(d => d.PurchaseReqCharges, o => o.MapFrom(src => src.PurchaseReqCharges)); } } public class PurchaseReqItemProfile : Profile { public PurchaseReqItemProfile() { CreateMap<PurchaseReqItem, PurchaseReqItemDto>(); CreateMap<PurchaseReqItemDto, PurchaseReqItem>() .ForMember(d => d.TaxRate, o => o.Ignore()); } } public class PurchaseReqChargeProfile : Profile { public PurchaseReqChargeProfile() { CreateMap<PurchaseReqCharge, PurchaseReqChargeDto>(); CreateMap<PurchaseReqChargeDto, PurchaseReqCharge>() .ForMember(d => d.OtherCharge, o => o.Ignore()); } }
AutoMapper注册
builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
仓储获取数据
public async Task<PurchaseReq> GetPrForUpdateAsync(string prNumber) { ArgumentNullException.ThrowIfNull(prNumber); return await context.PurchaseReqs .Include(pr => pr.PurchaseReqItems) .Include(pr => pr.PurchaseReqCharges) .SingleOrDefaultAsync(pr => pr.PrNumber == prNumber); }
更新API
[HttpPut] [CanUserUpdate(RolesConstant.PurchaseReq)] public async Task<IActionResult> UpdatePurchaseReq([FromBody] PurchaseReqDto purchaseReqDto) { if (!short.TryParse(User.Claims.FirstOrDefault(c => c.Type == "sub")?.Value,out var userId)) return Unauthorized(); var purchaseReq = await purchaseReqRepo.GetPrForUpdateAsync(purchaseReqDto.PrNumber); if (purchaseReq == null) return NotFound(); if (purchaseReq.IsSubmitted) return BadRequest(string.Format($"PR ({purchaseReq.PrNumber}) was already submitted for approval and can't be updated.")); mapper.Map(purchaseReqDto, purchaseReq, options => options.BeforeMap((prDto, pr) => { pr.RemoveUnMatchedItems(prDto.PurchaseReqItems); pr.RemoveUnMatchedCharges(prDto.PurchaseReqCharges); })); purchaseReq.UpdateBy(userId); await purchaseReqRepo.SaveChangesAsync(); return NoContent(); }
问题原因
- 集合属性不可修改:实体中集合属性使用
init访问器,无法在运行时替换整个集合,AutoMapper默认的集合映射逻辑会因此异常,转而创建新的实体实例。 - 实体跟踪冲突:从数据库加载的嵌套实体(如PurchaseReqCharge)已被EF Core跟踪,AutoMapper创建的同ID新实例会和已跟踪实例冲突。
- 导航属性映射问题:DTO中的导航属性(如PurchaseReqItemDto.PurchaseReq)被映射回实体时,可能引发不必要的关联和跟踪逻辑。
解决方案
方案1:修改实体集合访问器+配置AutoMapper正确映射集合
- 修改实体集合属性:将
init改为set,允许运行时修改集合引用:
public class PurchaseReq { ... public List<PurchaseReqItem> PurchaseReqItems { get; set; } = []; public List<PurchaseReqCharge> PurchaseReqCharges { get; set; } = []; }
- 忽略导航属性映射:在DTO到实体的映射中排除导航属性,避免循环引用和跟踪问题:
// PurchaseReqItemProfile CreateMap<PurchaseReqItemDto, PurchaseReqItem>() .ForMember(d => d.TaxRate, o => o.Ignore()) .ForMember(d => d.PurchaseReq, o => o.Ignore()); // PurchaseReqChargeProfile CreateMap<PurchaseReqChargeDto, PurchaseReqCharge>() .ForMember(d => d.OtherCharge, o => o.Ignore()) .ForMember(d => d.PurchaseReq, o => o.Ignore());
- 配置集合映射逻辑:让AutoMapper优先使用已跟踪的实体进行更新,新项则添加到集合:
public class PurchaseReqProfile : Profile { public PurchaseReqProfile() { CreateMap<PurchaseReq, PurchaseReqDto>(); CreateMap<PurchaseReqDto, PurchaseReq>() .ForMember(dest => dest.PurchaseReqItems, opt => opt.ResolveUsing((src, dest, _, context) => { foreach (var itemDto in src.PurchaseReqItems) { var existingItem = dest.PurchaseReqItems.FirstOrDefault(i => i.Id == itemDto.Id); if (existingItem != null) { context.Mapper.Map(itemDto, existingItem); } else { var newItem = context.Mapper.Map<PurchaseReqItem>(itemDto); dest.PurchaseReqItems.Add(newItem); } } return dest.PurchaseReqItems; })) .ForMember(dest => dest.PurchaseReqCharges, opt => opt.ResolveUsing((src, dest, _, context) => { foreach (var chargeDto in src.PurchaseReqCharges) { var existingCharge = dest.PurchaseReqCharges.FirstOrDefault(c => c.Id == chargeDto.Id); if (existingCharge != null) { context.Mapper.Map(chargeDto, existingCharge); } else { var newCharge = context.Mapper.Map<PurchaseReqCharge>(chargeDto); dest.PurchaseReqCharges.Add(newCharge); } } return dest.PurchaseReqCharges; })); } }
方案2:手动处理集合映射(更直观易调试)
- 修改PurchaseReqProfile:排除集合属性的自动映射:
CreateMap<PurchaseReqDto, PurchaseReq>() .ForMember(d => d.PurchaseReqItems, o => o.Ignore()) .ForMember(d => d.PurchaseReqCharges, o => o.Ignore());
- 在API中手动处理集合更新:
[HttpPut] [CanUserUpdate(RolesConstant.PurchaseReq)] public async Task<IActionResult> UpdatePurchaseReq([FromBody] PurchaseReqDto purchaseReqDto) { if (!short.TryParse(User.Claims.FirstOrDefault(c => c.Type == "sub")?.Value,out var userId)) return Unauthorized(); var purchaseReq = await purchaseReqRepo.GetPrForUpdateAsync(purchaseReqDto.PrNumber); if (purchaseReq == null) return NotFound(); if (purchaseReq.IsSubmitted) return BadRequest($"PR ({purchaseReq.PrNumber}) was already submitted for approval and can't be updated."); // 映射主实体基本属性 mapper.Map(purchaseReqDto, purchaseReq); // 处理PurchaseReqItems:删除不存在的项,更新现有项,添加新项 purchaseReq.RemoveUnMatchedItems(purchaseReqDto.PurchaseReqItems); foreach (var itemDto in purchaseReqDto.PurchaseReqItems) { var existingItem = purchaseReq.PurchaseReqItems.FirstOrDefault(i => i.Id == itemDto.Id); if (existingItem != null) { mapper.Map(itemDto, existingItem); } else { var newItem = mapper.Map<PurchaseReqItem>(itemDto); purchaseReq.PurchaseReqItems.Add(newItem); } } // 处理PurchaseReqCharges purchaseReq.RemoveUnMatchedCharges(purchaseReqDto.PurchaseReqCharges); foreach (var chargeDto in purchaseReqDto.PurchaseReqCharges) { var existingCharge = purchaseReq.PurchaseReqCharges.FirstOrDefault(c => c.Id == chargeDto.Id); if (existingCharge != null) { mapper.Map(chargeDto, existingCharge); } else { var newCharge = mapper.Map<PurchaseReqCharge>(chargeDto); purchaseReq.PurchaseReqCharges.Add(newCharge); } } purchaseReq.UpdateBy(userId); await purchaseReqRepo.SaveChangesAsync(); return NoContent(); }
内容的提问来源于stack exchange,提问作者xaifeen
相关产品推荐
相关产品推荐

