简体   繁体   English

映射子对象的实体框架实例跟踪错误 - 是否有优雅的解决方案?

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

Some 2 years+ ago I asked this question which was kindly solved by Steve Py.大约 2 年前,我问了这个问题,Steve Py 很好地解决了这个问题。

I am having a similar but different problem now when mapping with sub-objects.在使用子对象进行映射时,我现在遇到了类似但不同的问题。 I have had this issue a few times and worked around it, but facing doing so again, I can't help thinking there must be a more elegant solution.我遇到过几次这个问题并解决了它,但再次面对这样做,我不禁想到必须有一个更优雅的解决方案。 I am coding a memebership system in Blazor Wasm and wanting update membership details via a web-api.我正在 Blazor Wasm 中编写会员系统,并希望通过 web-api 更新会员详细信息。 All very normal.一切都很正常。 I have a library function to update the membership:我有一个图书馆 function 来更新会员资格:

            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) };
                }
            }

The Membership object is an EF DB object and references a many to many list of MembershipTypes: Membership object 是一个 EF DB object 并引用多对多的 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..
}

The MembershipLTDTO is a lightweight DTO with a few heavy objects removed. MembershipLTDTO 是一个轻量级 DTO,删除了一些重物。

Executing the code, I get an EF exception:执行代码,我得到一个 EF 异常:

The instance of entity type 'MembershipType' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked.无法跟踪实体类型“MembershipType”的实例,因为已跟踪另一个具有与 {'Id'} 相同键值的实例。 When attaching existing entities, ensure that only one entity instance with a given key value is attached.附加现有实体时,确保只附加一个具有给定键值的实体实例。

I think (from the previous question I asked some time ago) that I understand what is happening, and previously, I have worked around this by having a seperate function that would in this case update the membership types.我认为(根据我前段时间提出的上一个问题)我了解正在发生的事情,并且之前,我已经通过单独的 function 来解决这个问题,在这种情况下它会更新成员类型。 Then, stripping it out of the 'found' and 'sent' objects to allow Mapper to do the rest.然后,将其从“已找到”和“已发送”对象中剥离,以允许 Mapper 执行 rest。

In my mapping profile I have the mappings defines as follows for these object types:在我的映射配置文件中,我为这些 object 类型定义了如下映射:

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

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

As I was about to go and do that very thing again, I was wondering if I am missing a trick with my use of Mapper, or Entity Framework that would allow it to happen more seamlessly?当我即将 go 再次做那件事时,我想知道我是否错过了使用 Mapper 或 Entity Framework 的技巧,可以让它更无缝地发生?

A couple of things come to mind.我想到了几件事。 The first thing is that the call to context.Memberships.Update(foundmembership);第一件事是调用context.Memberships.Update(foundmembership); isn't required here so long as you haven't disabled tracking in the DbContext.只要您没有在 DbContext 中禁用跟踪,这里就不是必需的。 Calling SaveChanges will build an UPDATE SQL statement for whatever values change (if any) where Update will attempt to overwrite the entitiy(ies).调用SaveChanges将为Update将尝试覆盖实体的任何值更改(如果有)构建一个UPDATE SQL 语句。

The issue you are likely encountering is common when dealing with references, and I would recommend a different approach because of this.在处理引用时,您可能遇到的问题很常见,因此我建议采用不同的方法。 To outline this, lets look at Membership Types.为了概述这一点,让我们看一下成员资格类型。 These would typically be a known list that we want to associate to new and existing memberships.这些通常是我们想要关联到新的和现有的成员资格的已知列表。 We're not going to ever expect to create a new membership type as part of an operation where we create or update a membership, just add or remove associations to existing memberships.我们永远不会期望在创建或更新成员资格的操作中创建新的成员资格类型,只需添加或删除与现有成员资格的关联。

The problem with using Automapper for this is when we want to associate another membership type in our passed in DTO.为此使用 Automapper 的问题是当我们想要在传入的 DTO 中关联另一个成员类型时。 Say we have existing data that had a membership associated with Membership Type #1, and we want to add MemberShip Type #2.假设我们现有的数据具有与成员身份类型 #1 关联的成员身份,并且我们想要添加成员身份类型 #2。 We load the original entity types to copy values across, eager loading membership types so we get the membership and Type #1, so far so good.我们加载原始实体类型以复制值,预加载成员类型,因此我们获得成员和类型#1,到目前为止一切顺利。 However, when we call Mapper.Map() it sees a MemberShip Type #2 in the DTO, so it will add a new entity with ID #2 into the collection of our loaded Membership's Types collection.但是,当我们调用 Mapper.Map() 时,它会在 DTO 中看到一个 MemberShip Type #2,因此它会将 ID #2 的新实体添加到我们加载的 Membership 的 Types 集合中。 From here, one of three things can happen:从这里开始,可能会发生以下三种情况之一:

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.

The issue here is that Automapper doesn't have knowledge that any new Membership Type should be retrieved from the DbContext.这里的问题是 Automapper 不知道应该从 DbContext 中检索任何新的成员资格类型。

Using Automapper's Map method can be used to update child collections, though it should only be used to update references that are actual children of the top-level entity.使用 Automapper 的Map方法可用于更新子实体 collections,但它应该只用于更新作为顶级实体的实际子实体的引用。 For instance if you have a Customer and a collection of Contacts where updating the customer you want to update, add, or remove contact detail records because those child records are owned by, and explicitly associated to their customer.例如,如果您有一个客户和一个联系人集合,在其中更新客户,您想要更新、添加或删除联系人详细信息记录,因为这些子记录由他们的客户拥有,并明确关联到他们的客户。 Automapper can add to or remove from the collection, and update existing items. Automapper 可以添加到集合或从集合中删除,并更新现有项目。 For references like many-to-many/many-to-one we cannot rely on that since we will want to associate existing entities, not add/remove them.对于像多对多/多对一这样的引用,我们不能依赖它,因为我们想要关联现有实体,而不是添加/删除它们。

In this case, the recommendation would be to tell Automapper to ignore the Membership Types collection, then handle these afterwards.在这种情况下,建议是告诉 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();

For items being removed, we find those entities in the loaded data state and remove them from the collection.对于被删除的项目,我们在加载的数据 state 中找到这些实体,并将它们从集合中删除。 For new items being added, we go to the context, fetch them all, and add them to the loaded data state's collection.对于添加的新项目,我们 go 到上下文,获取它们,并将它们添加到加载数据状态的集合中。

Notwithstanding marking Steve Py's solution as the answer, because it is a solution that works, though not as 'elegant' as I would have liked.尽管将 Steve Py 的解决方案标记为答案,因为它是一个有效的解决方案,尽管不像我希望的那样“优雅”。

I was pointed in another direction however by the comment from然而,我被指向了另一个方向
Lucian Bargaoanu, which, though a little cryptic, after some digging I found could be made to work. Lucian Bargaoanu,虽然有点神秘,但经过一些挖掘后我发现它可以工作。

To do this I had to add 'AutoMapper.Collection' and 'AutoMapper.Collection.EntityFrameworkCore' to my solution.为此,我必须将“AutoMapper.Collection”和“AutoMapper.Collection.EntityFrameworkCore”添加到我的解决方案中。 There was a bit of jiggery pokery around setting it up as the example [here][2], didn't match up with my set up.将其设置为 [此处][2] 中的示例存在一些问题,与我的设置不匹配。 I used this in my program.cs:我在我的 program.cs 中使用了这个:

// Auto Mapper Configurations
var mappingConfig = new MapperConfiguration(mc =>
{
    mc.AddProfile(new MappingProfile());
    mc.AddCollectionMappers();
    
});

I also had to modify my mapping profile for the object - DTO mapping to this:我还必须修改 object 的映射配置文件 - DTO 映射到此:

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

Which is used to tell AutoMapper which fields to use for an equality.哪个用于告诉 AutoMapper 哪些字段用于相等。

I took out the context.Memberships.Update as recommended by Steve Py and it works.我按照 Steve Py 的建议取出了 context.Memberships.Update 并且它有效。

Posted on behalf of the question asker代表提问者发表

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM