如何在EF Core中使用AutoMapper正确映射多对多关系?
问题描述
我定义了以下两个类:
public class ADto { public int Id { get; set; } public List<BDto>? BSet { get; set; } } public class A { public int Id { get; set; } public virtual ICollection<B>? BSet { get; set; } }
我使用AutoMapper将传入的ADto转换为A后执行创建操作,AutoMapper映射配置如下:
CreateMap<A, ADto>(); CreateMap<B, BDto>();
业务代码:
public async Task<OperationResultDto> CreateAsync(ADto aDto, CancellationToken cancellationToken) { // Create & Map the record A a = new(); mapper.Map(aDto, a); // Create the object in database await repository.CreateAsync(a, cancellationToken); }
执行时出现错误:
SqlException: Cannot insert explicit value for identity column in table 'base_qamarks' when IDENTITY_INSERT is set to OFF.
目前我通过在AutoMapper中忽略BSet映射,再手动赋值的方式解决问题:
CreateMap<ADto, A>() .ForMember(dest => dest.BSet, opt => opt.Ignore());
手动赋值代码:
public async Task<OperationResultDto> CreateAsync(ADto aDto, CancellationToken cancellationToken) { // Create & Map the record A a = new(); mapper.Map(aDto, a); // Manually assign it foreach (BDto bDto in aDto.BSet) { B? b = await bRepository.GetEntityAsync(bDto.Id, cancellationToken); if (b != null) { a.BSet.Add(b); } } // Create the object in database await repository.CreateAsync(a, cancellationToken); }
该方法可行但存在冗余代码,请问如何通过AutoMapper实现多对多关系的正确映射?
解决方案
错误根源是:AutoMapper默认会把ADto.BSet中的BDto直接映射为全新的B实体,这些实体带有从BDto过来的已存在Id。EF Core会误判为要插入新的B记录,尝试给自增主键列赋值,从而触发报错。
以下是两种通过AutoMapper优化的方案:
方案一:映射时直接注入仓储处理关联
在ADto到A的映射配置中,自定义BSet的转换逻辑,注入仓储获取已存在的B实体:
// 配置ADto到A的映射,注入IBRepository CreateMap<ADto, A>() .ForMember(dest => dest.BSet, opt => opt.MapFrom(async (src, dest, _, context) => { var bRepo = context.GetService<IBRepository>(); var cancellationToken = context.Items["CancellationToken"] as CancellationToken? ?? CancellationToken.None; var bEntities = new List<B>(); if (src.BSet != null) { foreach (var bDto in src.BSet) { var b = await bRepo.GetEntityAsync(bDto.Id, cancellationToken); if (b != null) { bEntities.Add(b); } } } return bEntities; })); // 保留原有反向映射 CreateMap<A, ADto>(); CreateMap<B, BDto>();
调整业务代码,将CancellationToken传入AutoMapper上下文:
public async Task<OperationResultDto> CreateAsync(ADto aDto, CancellationToken cancellationToken) { var a = mapper.Map<A>(aDto, opts => opts.Items["CancellationToken"] = cancellationToken); await repository.CreateAsync(a, cancellationToken); // 返回对应的OperationResultDto }
方案二:使用自定义转换器解耦
创建独立的类型转换器,封装BDto到B实体的关联逻辑:
public class BSetConverter : IAsyncTypeConverter<List<BDto>, ICollection<B>> { private readonly IBRepository _bRepo; public BSetConverter(IBRepository bRepo) { _bRepo = bRepo; } public async Task<ICollection<B>> Convert(List<BDto> source, ICollection<B> destination, ResolutionContext context) { var cancellationToken = context.Items["CancellationToken"] as CancellationToken? ?? CancellationToken.None; var bEntities = new List<B>(); if (source != null) { foreach (var bDto in source) { var b = await _bRepo.GetEntityAsync(bDto.Id, cancellationToken); if (b != null) { bEntities.Add(b); } } } return bEntities; } }
在AutoMapper配置中注册转换器:
CreateMap<ADto, A>() .ForMember(dest => dest.BSet, opt => opt.ConvertUsing<BSetConverter>()); // 保留原有映射 CreateMap<A, ADto>(); CreateMap<B, BDto>();
业务代码同样需要传递CancellationToken到AutoMapper上下文,和方案一的业务代码一致。
两种方案都能自动完成多对多关系的实体关联,省去手动遍历的冗余代码,同时避免EF Core误插入已存在的B实体。
内容的提问来源于stack exchange,提问作者CorrieJanse
相关产品推荐
相关产品推荐

