我有以下两个课程:
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
,然后创建A。
自动映射器/映射配置文件:
CreateMap<A, ADto>();
CreateMap<B, BDto>();
代码:
public async Task<OperationResultDto> CreateAsync(A aDto, CancellationToken cancellationToken)
{
// Create & Map the record
A a = new();
mapper.Map(aDto, a);
// Create the object in database
await repository.CraeteAsync(a, cancellationToken);
}
我收到以下错误:
SqlException:当 IDENTITY_INSERT 设置为 OFF 时,无法在表“base_qamarks”中插入标识列的显式值。
现在我可以通过执行以下操作来解决这个问题 - 在 AutoMapper 中放置:
CreateMap<A, ADto>()
.ForMember(dest => dest.BSet, opt => opt.Ignore());
然后手动赋值:
public async Task<OperationResultDto> CreateAsync(A aDto, CancellationToken cancellationToken)
{
// Create & Map the record
A a = new();
mapper.Map(aDto, a);
// Manuall Assign it
foreach (BDto bDto in A.BSet)
{
B? b = await bRepository.GetEntityAsync(bDto.Id, cancellationToken);
if (b != null)
{
a.BSet.Add(b);
}
}
// Create the object in database
await repository.CraeteAsync(a, cancellationToken);
}
这可行,但它是额外的代码,看起来不太好。如何正确使用 AtuoMapper 来映射它?
Automapper 的 Map 调用将创建 A 和 B 的实例。问题在于,如果“B”是与现有行的关联并且不打算创建为 A 的子级,则 EF 期望每个“B”都是对跟踪的行的引用实体。
如果您在 A.B 中只关心 B 引用是否关联,而不是更新“B”记录的内容,那么您可以附加 B,但您的存储库模式很可能会妨碍的这个。使用范围
DbContext
它看起来像:
public async Task<OperationResultDto> CreateAsync(A aDto, CancellationToken cancellationToken)
{
using var context = DbContextFactory.Create();
// include mappings for A and B.
A a = mapper.Map<A>(aDto);
foreach(var b in a.Bs)
context.Attach(b);
context.As.Add(a);
await context.SaveChangesAsync();
}
这里需要注意的是,DbContext 实例的范围仅限于此操作,如果是针对请求,则此代码可能仍然有效,但任何可能跟踪附加的 B 实例的操作都可能导致有关实例的异常B 已被跟踪。这种异常是视情况而定的,并在运行时出现。如果使用请求范围的 DbContext,那么您需要首先搜索跟踪缓存,如果发现安全则进行替换。例如,如果使用注入的、请求范围的 DbContext 而不是工厂创建的
using
范围实例:
public async Task<OperationResultDto> CreateAsync(A aDto, CancellationToken cancellationToken)
{
// include mappings for A and B.
A a = mapper.Map<A>(aDto);
foreach(var b in a.Bs)
{
B? existingB = _context.Bs.Local.FirstOrDefault(x => x.Id = b.Id);
if(existingB != null)
{
a.BSet.Remove(b);
a.BSet.Add(existingB);
}
else
_context.Attach(b);
}
_context.As.Add(a);
await _context.SaveChangesAsync();
}
丑陋,当使用存储库抽象 DbContext 时可能更丑陋。
为了安全起见,我通常建议插入关联时:
public async Task<OperationResultDto> CreateAsync(A aDto, CancellationToken cancellationToken)
{
var bIds = aDto.BSet.Select(b => b.Id).ToList();
A a = mapper.Map<A>(aDto); // with just ADto mapping
var bs = await _context.Bs
.Where(b => bIds.Contains(b.Id))
.ToListAsync();
foreach(var b in bs)
a.BSet.Add(b);
_context.As.Add(a);
await _context.SaveChangesAsync();
}
这意味着发送到“Create”方法的 Dto 可以简化为仅传递 BId 来与新的 A 关联,而不是发送完全序列化的 B DTO。我们不必担心跟踪的实例,这也可以更容易地应用于您的存储库抽象中。这避免了 Select N+1,其中您的初始解决方案是通过在一次调用中获取所有 B 来循环运行对每个 B 的查询。
在执行可能添加或删除 B 关联的更新场景时,您希望获取包括所有当前关联的 B 在内的 A,确定需要添加或删除哪些 B,然后删除不在 DTO 列表中的所有 B并从要添加到 BSet 集合的 DbContext 中获取要添加的 B。这不是用新的 B 集合替换 A.BSet 列表的问题,这会导致异常或插入重复数据。
值得注意的是,您调用的初始 Map(src, dest) 实现对于实体的更新场景很有用,在该场景中,您从数据库获取现有 A,然后调用
.Map(aDto, existingA)
将预期值从 DTO 传输到实体。要创建 A 的实例(插入),您可以将其简化为 Map<A>(aDto)
。