EF Core执行投影操作时处理对象引用的正确方式是什么
推荐方案:自定义映射+本地缓存字典保留引用一致性
你这个场景明确要求投影实例引用一致、后续还要反向映射回实体,EF Core默认投影机制满足不了需求,直接走自定义映射保留引用的方案即可,实现成本很低,也符合EF Core的设计预期。
具体实现步骤
- 先查询实体集合:你已经开启了
AsNoTrackingWithIdentityResolution(),EF Core会自动保证返回的实体层面,所有相同Id的Item都是同一个实例,无需额外处理实体层的引用重复问题:
var items = await context.Items .Include(x => x.ChildItem) .AsNoTrackingWithIdentityResolution() .ToListAsync();
- 映射时用字典缓存已生成的投影实例:遍历实体集合时按Id存储已经创建好的
ItemProjection,同一个Id仅生成一次实例,父子引用直接从字典中读取,保证所有指向同一个子项的父级都使用同一个投影实例:
var projectionCache = new Dictionary<int, ItemProjection>(); // 先创建所有实体对应的投影,存入缓存 foreach (var item in items) { if (!projectionCache.ContainsKey(item.Id)) { projectionCache[item.Id] = new ItemProjection { Id = item.Id // 其他字段映射直接在此处添加即可 }; } } // 再批量处理导航属性的引用关系 foreach (var item in items) { if (item.ChildItem != null) { projectionCache[item.Id].ChildItem = projectionCache[item.ChildItem.Id]; } } // 最终需要的投影集合 var projectionResult = projectionCache.Values.ToList();
可选简化实现(基于AutoMapper)
如果项目中已经用了AutoMapper做映射,不需要自己手写缓存逻辑,开启PreserveReferences配置就能自动实现引用一致性:
var mapperConfig = new MapperConfiguration(cfg => { cfg.CreateMap<Item, ItemProjection>() .PreserveReferences(); // 该配置会自动维护映射过程中的引用一致 }); var mapper = mapperConfig.CreateMapper(); var projectionResult = mapper.Map<List<ItemProjection>>(items);
方案优势
- 完全符合引用一致性要求:同一个子项的投影在全集合中仅存在一个实例,哪怕子项本身也作为根元素出现在返回集合中也不会生成重复实例
- 后续反向映射回实体更简单:修改后的投影直接按Id匹配原实体即可,不需要处理重复实例的更新冲突
- 性能开销极低:仅多了两次遍历和字典查询操作,开销远小于EF Core查询本身的消耗
不推荐默认EF Core投影的原因
EF Core的投影逻辑设计上就没有提供引用一致性的保障,AsNoTrackingWithIdentityResolution配置仅作用于实体实例,不会影响投影的生成逻辑,目前包括EF Core 8在内的所有版本,都没有支持投影层面引用统一的官方配置,硬要使用默认投影的话还需要额外做实例合并,反而比自定义映射更麻烦。
反向映射回实体的注意事项
修改完投影需要更新回数据库时,直接用投影的Id匹配原始查询的实体集合(或者重新查询开启跟踪的实体)即可,因为所有父级引用的都是同一个子项投影实例,只需要更新一次对应Id的实体,不会出现重复更新冲突。
内容的提问来源于stack exchange,提问作者quain
相关产品推荐
相关产品推荐

