使用 Automapper 在 EF core 中映射 M:M 关系

问题描述 投票:0回答:1

我有以下两个课程:

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 来映射它?

c# entity-framework automapper
1个回答
0
投票

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)

© www.soinside.com 2019 - 2024. All rights reserved.