Entity Framework 映射子对象的实例跟踪错误 - 有没有优雅的解决方案?

Entity Framework Instance tracking error with mapping sub-objects - is there an elegant solution?

大约 2 年前,我问过这个 ,Steve Py 很好地解决了这个问题。

我在使用子对象进行映射时遇到了类似但不同的问题。我遇到过几次这个问题并解决了它,但再次面对这样做,我不禁想到必须有一个更优雅的解决方案。我正在 Blazor Wasm 中编写会员系统,并希望通过 web-api 更新会员详细信息。一切都很正常。 我有一个库函数来更新成员资格:

            public async Task<MembershipLTDTO> UpdateMembershipAsync(APDbContext context, MembershipLTDTO sentmembership)
            {
                Membership? foundmembership = context.Memberships.Where(x =>x.Id == sentmembership.Id)
                    .Include(x => x.MembershipTypes)
                    .FirstOrDefault();
                if (foundmembership == null)
                {
                    return new MembershipLTDTO { Status = new InfoBool(false, "Error: Membership not found", InfoBool.ReasonCode.Not_Found) };
                }
                try
                {  
                    _mapper.Map(sentmembership, foundmembership, typeof(MembershipLTDTO), typeof(Membership));
                    //context.Entry(foundmembership).State = EntityState.Modified;  <-This was a 'try-out'
                    context.Memberships.Update(foundmembership);
                    await context.SaveChangesAsync();
                    sentmembership.Status = new InfoBool(true, "Membership successfully updated");
                    return sentmembership;
                }
                catch (Exception ex)
                {
                    return new MembershipLTDTO { Status = new InfoBool(false, $"{ex.Message}", InfoBool.ReasonCode.Not_Found) };
                }
            }

Membership 对象是一个 EF DB 对象并引用多对多的 MembershipTypes 列表:

public class Membership
{
    [Key]
    public int Id { get; set; }

    ...more stuff...

    public List<MembershipType>? MembershipTypes { get; set; }   // The users membership can be several types. e.g. Employee + Director + etc..
}

MembershipLTDTO 是一个轻型 DTO,删除了一些重对象。

执行代码时,出现 EF 异常:

The instance of entity type 'MembershipType' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.

我认为(根据我前段时间提出的上一个问题)我了解正在发生的事情,并且之前,我通过使用一个单独的函数来解决这个问题,在这种情况下会更新成员类型。然后,将它从 'found' 和 'sent' 对象中剥离出来,让 Mapper 完成剩下的工作。

在我的映射配置文件中,我为这些对象类型定义了如下映射:

            CreateMap<Membership, MembershipLTDTO>();
            CreateMap<MembershipLTDTO, Membership>();

            CreateMap<MembershipTypeDTO, MembershipType>();
            CreateMap<MembershipType, MembershipTypeDTO>();

当我正准备再次做那件事时,我想知道我是否错过了使用 Mapper 的技巧,或者 Entity Framework 可以让它更无缝地发生?

我想到了几件事。首先,只要您没有在 DbContext 中禁用跟踪,这里就不需要调用 context.Memberships.Update(foundmembership);。调用 SaveChanges 将为任何值更改(如果有)构建一个 UPDATE SQL 语句,其中 Update 将尝试覆盖实体。

您在处理引用时可能遇到的问题很常见,因此我建议采用不同的方法。为了概述这一点,让我们看一下成员资格类型。这些通常是我们想要关联到新的和现有的成员资格的已知列表。我们永远不会期望在创建或更新成员资格的操作中创建新的成员资格类型,只需添加或删除与现有成员资格的关联。

为此使用 Automapper 的问题是当我们想要在传入的 DTO 中关联另一个成员类型时。假设我们现有的数据具有与成员身份类型 #1 关联的成员身份,并且我们想要添加成员身份类型 #2。我们加载原始实体类型以复制值,预加载成员类型,因此我们获得成员和类型#1,到目前为止一切顺利。但是,当我们调用 Mapper.Map() 时,它会在 DTO 中看到一个 MemberShip Type #2,因此它会将 ID #2 的新实体添加到我们加载的 Membership 类型集合的集合中。从这里开始,可能会发生以下三种情况之一:

1) The DbContext was already tracking an instance with ID #2 and 
will complain when Update tries to associate another entity reference 
with ID #2.

2) The DbContext isn't tracking an instance, and attempts to add #2 
as a new entity. 

  2.1) The database is set up for an Identity column, and the new 
membership type gets inserted with the next available ID. (I.e. #16)

  2.2) The database is not set up for an Identity column and the
 `SaveChanges` raises a duplicate constraint error.

这里的问题是 Automapper 不知道应该从 DbContext 中检索任何新的成员资格类型。

使用 Automapper 的 Map 方法可用于更新子集合,但它应该只用于更新作为 top-level 实体的实际子项的引用。例如,如果您有一个客户和一个联系人集合,在其中更新客户,您想要更新、添加或删除联系人详细信息记录,因为这些子记录由他们的客户拥有,并明确关联到他们的客户。 Automapper 可以添加到集合或从集合中删除,并更新现有项目。对于像 many-to-many/many-to-one 这样的引用,我们不能依赖它,因为我们想要关联现有实体,而不是 add/remove 它们。

在这种情况下,建议是让 Automapper 忽略 Membership Types 集合,然后再处理它们。

_mapper.Map(sentmembership, foundmembership, typeof(MembershipLTDTO), typeof(Membership));

var memberShipTypeIds = sentmembership.MembershipTypes.Select(x => x.MembershipTypeId).ToList();
var existingMembershipTypeIds = foundmembership.MembershipTypes.Select(x => x.MembershipTypeId).ToList();
var idsToAdd = membershipTypeIds.Except(existingMembershipTypeIds).ToList();
var idsToRemove = existingMembershipTypeIds.Except(membershipTypeIds).ToList();

if(idsToRemove.Any())
{
    var membershipTypesToRemove = foundmembership.MembershipTypes.Where(x => idsToRemove.Contains(x.MembershipTypeId)).ToList();
    foreach (var membershipType in membershipTypesToRemove)
        foundmembership.MembershipTypes.Remove(membershipType;
}
if(idsToAdd.Any())
{
    var membershipTypesToAdd = context.MembershipTypes.Where(x => idsToRemove.Contains(x.MembershipTypeId)).ToList();
    foundmembership.MembershipTypes.AddRange(membershipTypesToAdd); // if declared as List, otherwise foreach and add them.
}

context.SaveChanges();

对于要删除的项目,我们会找到处于加载数据状态的实体,并将它们从集合中删除。对于正在添加的新项目,我们转到上下文,将它们全部获取,并将它们添加到已加载数据状态的集合中。

尽管将 Steve Py 的解决方案标记为答案,因为它是一个有效的解决方案,尽管不像我希望的那样 'elegant'。

然而
的评论将我指向了另一个方向 Lucian Bargaoanu,虽然有点神秘,但经过一些挖掘后我发现它可以工作。

为此,我必须将 'AutoMapper.Collection' 和 'AutoMapper.Collection.EntityFrameworkCore' 添加到我的解决方案中。将其设置为 [此处][2] 中的示例存在一些问题,与我的设置不匹配。我在 program.cs:

中使用了这个
// Auto Mapper Configurations
var mappingConfig = new MapperConfiguration(mc =>
{
    mc.AddProfile(new MappingProfile());
    mc.AddCollectionMappers();
    
});

我还必须修改对象的映射配置文件 - DTO 映射到此:

            //Membership Types
            CreateMap<MembershipTypeDTO, MembershipType>().EqualityComparison((mtdto, mt) => mtdto.Id == mt.Id);

用于告诉 AutoMapper 哪些字段用于相等。

我取出了 Steve Py 推荐的 context.Memberships.Update 并且有效。

代表提问者发表