You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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();
}

问题原因

  1. 集合属性不可修改:实体中集合属性使用init访问器,无法在运行时替换整个集合,AutoMapper默认的集合映射逻辑会因此异常,转而创建新的实体实例。
  2. 实体跟踪冲突:从数据库加载的嵌套实体(如PurchaseReqCharge)已被EF Core跟踪,AutoMapper创建的同ID新实例会和已跟踪实例冲突。
  3. 导航属性映射问题:DTO中的导航属性(如PurchaseReqItemDto.PurchaseReq)被映射回实体时,可能引发不必要的关联和跟踪逻辑。

解决方案

方案1:修改实体集合访问器+配置AutoMapper正确映射集合

  1. 修改实体集合属性:将init改为set,允许运行时修改集合引用:
public class PurchaseReq
{
    ...
    public List<PurchaseReqItem> PurchaseReqItems { get; set; } = [];
    public List<PurchaseReqCharge> PurchaseReqCharges { get; set; } = [];
}
  1. 忽略导航属性映射:在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());
  1. 配置集合映射逻辑:让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:手动处理集合映射(更直观易调试)

  1. 修改PurchaseReqProfile:排除集合属性的自动映射:
CreateMap<PurchaseReqDto, PurchaseReq>()
    .ForMember(d => d.PurchaseReqItems, o => o.Ignore())
    .ForMember(d => d.PurchaseReqCharges, o => o.Ignore());
  1. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.25 12:54:52