使用Mapster映射EF Core实体图导致列表项状态为Detached而非Modified
问题场景
从数据库获取跟踪状态的Request实体图(包含关联的Materials集合),执行dto.Adapt(requestGraph)将DTO映射到该实体图后:
- Materials列表被更新,但列表中带Id的Material实体状态为
Detached - 调用
SaveChanges()时抛出异常:
The instance of entity type 'Material' cannot be tracked because another instance with the key value '{Id: 2}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.
核心原因
Mapster默认映射集合时会创建新的Material实例,而原实体图中的Material已经被EF跟踪,导致同一个Id存在两个不同实例,触发跟踪冲突。
解决办法
方案1:配置Mapster复用已跟踪实体
给Mapster设置映射规则,让它优先使用DbContext中已跟踪的实体,避免新建重复实例。
示例代码:
// 全局配置Mapster映射规则 TypeAdapterConfig<MaterialDto, Material>.NewConfig() .MapToTargetWith((dto, _, context) => { // 从DbContext的本地跟踪集合中查找对应Id的Material var dbContext = context.GetParameter<DbContext>("DbContext"); var existingMaterial = dbContext?.Set<Material>().Local.FirstOrDefault(m => m.Id == dto.Id); // 找到就复用,否则新建 return existingMaterial ?? new Material(); }); // 映射时传入DbContext参数 dto.Adapt(requestGraph, config => config.AddParameters("DbContext", _dbContext));
方案2:手动同步集合与实体状态
放弃Mapster自动处理集合,手动对比DTO和原实体的差异,直接更新已跟踪的实体:
// 取出原跟踪的Materials集合 var existingMaterials = requestGraph.Materials.ToList(); var dtoMaterials = dto.Materials; // 更新已有Material foreach (var dtoMat in dtoMaterials.Where(m => m.Id != 0)) { var existingMat = existingMaterials.FirstOrDefault(m => m.Id == dtoMat.Id); if (existingMat != null) { // 直接映射到已跟踪的实体,EF会自动标记为Modified dtoMat.Adapt(existingMat); } } // 添加新Material(Id未设置的项) var newMaterials = dtoMaterials.Where(m => m.Id == 0) .Select(dtoMat => dtoMat.Adapt<Material>()) .ToList(); requestGraph.Materials.AddRange(newMaterials); // 删除不存在于DTO中的Material var materialsToRemove = existingMaterials.Where(m => !dtoMaterials.Any(dtoMat => dtoMat.Id == m.Id)) .ToList(); foreach (var mat in materialsToRemove) { requestGraph.Materials.Remove(mat); // 软删除场景替换为:mat.IsDeleted = true; }
这种方式完全利用EF的跟踪机制,不会产生实例冲突,可控性更强。
方案3:关闭原实体跟踪(临时 workaround)
如果不需要原实体的跟踪状态,查询时用AsNoTracking(),之后手动设置所有实体状态:
// 查询时关闭跟踪 var requestGraph = _dbContext.Requests.AsNoTracking() .Include(r => r.Materials) .FirstOrDefault(r => r.Id == requestId); // 执行映射 dto.Adapt(requestGraph); // 手动设置实体状态 _dbContext.Entry(requestGraph).State = EntityState.Modified; foreach (var mat in requestGraph.Materials) { _dbContext.Entry(mat).State = mat.Id != 0 ? EntityState.Modified : EntityState.Added; } _dbContext.SaveChanges();
注意:此方案需要手动维护所有实体状态,容易遗漏,仅适合简单场景。
关键提醒
处理EF跟踪的实体集合时,禁止Mapster创建与已跟踪实体同Key的新实例,这是冲突的核心。优先选择方案1或方案2,稳定性更高。
内容的提问来源于stack exchange,提问作者Simple Code

