The instance of entity type 'UserLocation' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked

huangapple go评论62阅读模式
英文:

The instance of entity type 'UserLocation' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked

问题

Here's the translated content without the code:

在使用AspNetCore7 Blazor WASM应用程序与带有EF Core 7的AspNetCore7 API相配合时,使用Automapper在模型和DTO之间进行映射。

当我尝试执行一个PUT端点方法时,我收到以下错误消息:实体类型的实例“UserLocation”无法被跟踪,因为已经有另一个具有相同键值{‘Id’}的实例正在被跟踪。在附加现有实体时,请确保只有一个具有给定键值的实体实例被附加。考虑使用“DbContextOptionsBuilder.EnableSensitiveDataLogging”来查看冲突的键值。

我的模型:

public class ApiUser : IdentityUser
{
    ...
    public virtual List<UserLocation> Locations { get; set; } = new();
}

public class UserLocation
{
    public Guid Id { get; set; }
    [ForeignKey(nameof(User))]
    public string UserId { get; set; }
    public virtual ApiUser User { get; set; }
    ...
    public virtual UserLocationStat Stat { get; set; }
}

[PrimaryKey(nameof(UserLocationId))]
public class UserLocationStat
{
    [ForeignKey(nameof(Location))]
    public Guid UserLocationId { get; set; }
    public virtual UserLocation Location { get; set; }
    ...
}

我的DTOs:

public class UserEditAccountDto
{
    public string Id { get; set; } = string.Empty;
    public List<UserLocationDto> Locations { get; set; } = new();
}

public class UserLocationDto : UdbObjectDto
{
    public Guid Id { get; set; }
    public string? UserId { get; set; }
    ...
    public UserLocationStatDto? Stat { get; set; }
}

public class UserLocationStatDto
{
    public Guid UserLocationId { get; set; }
    ...
}

Automapper服务扩展:

public static void ConfigureAutoMapper(this IServiceCollection services)
{
    services.AddAutoMapper(Assembly.GetExecutingAssembly());
}

Automapper初始化程序:

public class MapperInitializer : Profile
{
    public MapperInitializer()
    {
        CreateMap<ApiUser, UserEditAccountDto>().ReverseMap();
        CreateMap<UserLocation, UserLocationDto>().ReverseMap();
        CreateMap<UserLocationStat, UserLocationStatDto>().ReverseMap();
    }
}

API端点:

[HttpPut]
[Route("edit-user")]
public async Task<IActionResult> EditUser(UserEditAccountDto userEditAccountDto)
{
    //获取要更新的用户
    var apiUser = await _context.Users
        .Where(x => x.Id == userEditAccountDto.Id)
        .Include(x => x.Locations).ThenInclude(loc => loc.Stat)
        .FirstOrDefaultAsync();

    if (apiUser == null)
        return BadRequest();

    //将DTO映射到实体
    _mapper.Map(userEditAccountDto, apiUser);

    //保存
    await _context.SaveChangesAsync();

    return NoContent();
}

如果我删除_mapper.Map(userEditAccountDto, apiUser);这一行,只需手动更新apiUser的属性,保存操作await _context.SaveChangesAsync();就能正常工作。看起来这似乎是一个Automapper的问题。要么我不理解如何正确使用Automapper,要么我的模型/DTO没有正确设置。有经验的人能否看一下并提供建议?

英文:

AspNetCore7 Blazor WASM app paired with an AspNetCore7 API with EF Core 7, using Automapper between Model and DTO.

When I attempt to execute a PUT endpoint method, I get the following error: The instance of entity type &#39;UserLocation&#39; cannot be tracked because another instance with the same key value for {&#39;Id&#39;} is already being tracked.
When attaching existing entities, ensure that only one entity instance with a given key value is attached.
Consider using &#39;DbContextOptionsBuilder.EnableSensitiveDataLogging&#39; to see the conflicting key values.

My Model:

public class ApiUser : IdentityUser
{
    ...
    public virtual List&lt;UserLocation&gt; Locations { get; set; } = new();
}

public class UserLocation
{
    public Guid Id { get; set; }
    [ForeignKey(nameof(User))]
    public string UserId { get; set; }
    public virtual ApiUser User { get; set; }
    ...
    public virtual UserLocationStat Stat { get; set; }
}

[PrimaryKey(nameof(UserLocationId))]
public class UserLocationStat
{
    [ForeignKey(nameof(Location))]
    public Guid UserLocationId { get; set; }
    public virtual UserLocation Location { get; set; }
    ...
}

My DTOs:

public class UserEditAccountDto
{
    public string Id { get; set; } = string.Empty;
    public List&lt;UserLocationDto&gt; Locations { get; set; } = new();
}

public class UserLocationDto : UdbObjectDto
{
    public Guid Id { get; set; }
    public string? UserId { get; set; }
    ...
    public UserLocationStatDto? Stat { get; set; }
}

public class UserLocationStatDto
{
    public Guid UserLocationId { get; set; }
    ...
}

Automapper service extension:

public static void ConfigureAutoMapper(this IServiceCollection services)
{
    services.AddAutoMapper(Assembly.GetExecutingAssembly());
}

Automapper initializer:

public class MapperInitializer : Profile
{
    public MapperInitializer()
    {
        CreateMap&lt;ApiUser, UserEditAccountDto&gt;().ReverseMap();
        CreateMap&lt;UserLocation, UserLocationDto&gt;().ReverseMap();
        CreateMap&lt;UserLocationStat, UserLocationStatDto&gt;().ReverseMap();
    }
}

API Endpoint:

[HttpPut]
[Route(&quot;edit-user&quot;)]
public async Task&lt;IActionResult&gt; EditUser(UserEditAccountDto userEditAccountDto)
{
    //get user for update
    var apiUser = await _context.Users
        .Where(x =&gt; x.Id == userEditAccountDto.Id)
        .Include(x =&gt; x.Locations).ThenInclude(loc =&gt; loc.Stat)
        .FirstOrDefaultAsync();

    if (apiUser == null)
        return BadRequest();

    //map dto to entity
    _mapper.Map(userEditAccountDto, apiUser);

    //SAVE
    await _context.SaveChangesAsync();

    return NoContent();
}

If I remove the line _mapper.Map(userEditAccountDto, apiUser); and just manually update a property of apiUser the save operation await _context.SaveChangesAsync(); works. It seems it's an Automapper issue. Either I don't understand how to properly use Automapper, or my model/DTOs aren't set up properly. Can someone with more experience take a look and advise?

答案1

得分: 4

这可能是DTO/entities作为聚合根的一个限制或问题。(包含UserLocations)您正在使用实体急切加载现有的用户位置,这是一个良好的必要的第一步,如果缺少这一点,将会是一个明显的问题。

Automapper在处理聚合根时的行为会根据它是否设置了相关实体的映射配置而变化。在类似的复制场景中(A.Child -> B.Child),如果Automapper为Child(在您的情况下为UserLocationStat和UserLocation以及它们各自的DTO)设置了映射配置,那么复制将是一个深度复制,A的子项的值将被复制到B的引用中。如果Automapper没有为子项声明映射,那么它将进行浅复制,通常会导致B的子项引用设置为A的,但考虑到我们要让Automapper从DTO过渡到实体,这可能会导致创建一个新的UserLocationStat/UserLocation实体并将其添加到B中。(这可能会导致已跟踪实例的错误)您需要第一种情况,所以确保您的Automapper配置为UserLocationStat和UserLocation声明了映射。这可能是您的情况中缺少的部分。

一个更大的问题可能是,如果您的请求通过DTO向用户添加位置信息。在正常的父子关系(比如订单和订单项)中,子项集合基本上由父项拥有,只要子项本身没有引用任何不属于它的东西,这是可以接受的。如果UserLocations是或包含类似查找引用的东西,这是一行将通过多个不同用户的位置引用的一行,那么为一个对于该用户是新的UserLocation添加一个UserLocationStat,但引用一个现有的行将会引发问题。Automapper不知道“嘿,这是一条现有记录,我想要创建一个关联到这个用户的记录,从DbContext中获取UserLocation(或其他相关实体)并创建一个新的UserLocationStat链接到它”。它将尝试创建一个新的UserLocationStat和一个新的UserLocation。如果DbContext碰巧正在跟踪UserLocation,您将遇到相同的问题,尽管进行了深度复制映射。

简而言之,在使用Automapper复制聚合根中的数据将会极具情境性,并且除了在进行类似复制的情况下,最好只更改聚合内的值,不要添加/删除关系,特别是涉及到其他聚合/实体之间共享引用的情况下,可能应该避免使用它。Automapper在研究数据在DTO和实体之间如何移动时代表一个黑匣子,因此最好只在简单情况下依赖它。对于更复杂的实体和情景,我建议使用代码手动处理更新,因为这样更容易跟踪各种情景下正在执行的操作,并发现潜在的问题。

英文:

This is probably a limitation or issue with the fact that the DTO/entities are an aggregate root. (contains UserLocations) You are eager loading the existing user locations with the entity which is a good required first step and missing that would have been an obvious problem.

Automapper's behaviour when dealing with aggregate roots varies depending on whether it has mapping configurations set up for the related entities. In a like-for-like copy scenario (A.Child -> B.Child) if Automapper has a mapping configuration for the Child (in your case UserLocationStat and UserLocation with their respective DTOs) then the copy will be a deep copy where the values from A's child will be copied into B's reference. If Automapper does not have a mapping declared for the Children, then it will shallow copy which would normally see B's child reference set to A's, but given we are having automapper transition from DTO to entity, that would probably result in a new UserLocationStat/UserLocation entity being created and added to B. (Which would likely result in an error around already tracked instances) You would want the first scenario, so ensuring that your Automapper configuration has mapping declarations for UserLocationStat and UserLocation. This is potentially what is missing in your case.

A bigger problem will be if your request is doing things like adding Locations to a user in the request through the DTO. In a normal parent-child relationship (think Orders and OrderItems) where the child collection is essentially owned by the parent, this is Ok provided that the child doesn't itself reference anything that isn't owned. If UserLocations are or contain something like a lookup reference which is a row that would be referenced through several different User's Locations, adding a UserLocationStat for a UserLocation new to that User but referencing an existing row will cause a problem. Automapper does not know "hey, this is an existing record I want to create an association to this User, fetch the UserLocation (or other related entity) from the DbContext and create a new UserLocationStat linking to it". It would attempt to create a new UserLocationStat and a new UserLocation. If the DbContext happened to be tracking the UserLocation you run into the same problems despite having the deep copy mapping.

In a nutshell, copying data in aggregate roots with Automapper is going to be extremely situational, and probably should be avoided except for situations where you are doing like-for-like copying where ideally only values within the aggregate are being changed, not adding/removing relationships, especially anything involving references shared between other aggregates/entities. Automapper represents a black box when investigating how data is moving between a DTO and Entity so ideally it should only really be relied on for simple scenarios. For more complex entities and scenarios I recommend using code to manually handle updates as it's much easier to follow what is being done for various scenarios and spot potential issues.

huangapple
  • 本文由 发表于 2023年4月13日 22:26:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/76006608.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定