EF: 实体类型X的实例无法被跟踪,因为已经有另一个具有相同键的此类型实例正在被跟踪。

4
我将在HTTP请求中以Json格式发送一个用户实体,如下所示:

POST http://localhost:52054/api/Authentication/DeleteAccessToken HTTP/1.1
Host: localhost:52054
Content-Type: application/json

{"id":1,"userName":"mnoureldin","accessToken":{"id":1,"token":"123ABC456EFG","userId":1}}

我的控制器(使用EF Core)的处理方式如下:

[HttpPost]
public IActionResult DeleteAccessToken([FromBody]User user)
{
    using (var Context = new UnitOfWork().Context)
    {
        var userEntity = Context.Users.Find(user.Id); // Get the real entity of the user received as json
        if (userEntity != null)
        {
            var accessTokenEntity = Context.AccessTokens.Find(userEntity.AccessToken.Id); // Find the entity of the accesstoken (I tried also directly by accessing the navigation property of user entity)
            Context.AccessTokens.Remove(accessTokenEntity);
            return Ok();
        }
        else
        {
            return Unauthorized();
        }
    }
}

但是这行代码 Context.AccessTokens.Remove(accessTokenEntity); 抛出了以下异常:

类型为“System.InvalidOperationException”的异常在 Microsoft.EntityFrameworkCore.dll 中发生,但未在用户代码中处理。

其他信息:实体类型“AccessToken”的实例无法被跟踪,因为已经有另一个具有相同键值的该类型实例正在被跟踪。对于大多数键类型,如果没有设置键(即如果为键属性分配其类型的默认值),则将创建唯一的临时键值来添加新实体。如果您明确为新实体设置键值,请确保它们不会与现有实体或为其他新实体生成的临时值发生冲突。当附加现有实体时,请确保只有一个具有给定键值的实体实例附加到上下文。

我还尝试从userEntity直接访问AccessToken导航属性,但遇到了同样的异常。

这是我的UnitOfWork初始化:

public UnitOfWork()
{
    // Configure EF connection
    var optionsBuilder = new DbContextOptionsBuilder<CustomDbContext>();
    optionsBuilder
        .UseMySQL(@"server=192.168.1.35; port=3306; sslmode=none;
                    userid=root;
                    pwd=P@ssword;
                    database=dotnet;");

    Context = new CustomDbContext(optionsBuilder.Options);

    // Configure data loading method to explicit
    Context.AccessTokens.Load();
}

我的CustomBdContext:

public class CustomDbContext : DbContext
{
    // Tell EF to map the entities to tables
    public DbSet<User> Users { get; set; }
    public DbSet<AccessToken> AccessTokens { get; set; }

    public CustomDbContext(DbContextOptions options) : base(options)
    {
    }
}

我有一个简单的数据模型,它包含一对一关系:

User ----- AccessToken

用户:

public class User
{
    public int Id { get; set; }
    public string UserName { get; set; }

    public virtual AccessToken AccessToken { get; set; }
}

访问令牌:

public class AccessToken
{
    public int Id { get; set; }
    public string Token { get; set; }

    [ForeignKey("User"), Required]
    public int UserId { get; set; }
    public virtual User User { get; set; }
}

有人能帮我解决这个问题吗?我不明白到底发生了什么。


好吧,事实证明在你的情况下是有所不同的(我敢打赌这是Context.AccessTokens.Load - 你为什么要这样做),所以我删除了那个评论。 - Evk
尽管在上下文工厂(你的“UnitOfWork”实质上就是)中执行Context.AccessTokens.Load();似乎很奇怪,但这仍然无法解释该错误。当前可见的代码似乎没有导致任何附加实体的重复。 Find始终会首先查找已附加的实体。 - Gert Arnold
@GertArnold请查看上一条评论,但如果不是在UnitOfWork中,您会把.load()放在哪里?当没有可用的延迟加载时,如何加载每个实体的导航属性?对于每个语句使用include吗? - Mohammed Noureldin
1
我仍然觉得我们没有看到整个画面。但是我会将其建模为真正的1:1关系(即AccessToken的PK也是它与用户的FK)。 - Gert Arnold
2
通过EF从数据库中获取的任何对象,无论是通过Find、LINQ还是Load,都会存储在上下文的缓存中,并跟踪其更改(除非您主动禁用跟踪)。上下文是一个身份映射,因此只能跟踪每个实体的一个副本。当EF获取实体时,它会自行处理这个身份规则。手动将实体附加到上下文(例如通过添加它们)可能会导致问题。但在可见代码中似乎并没有发生这种情况。也许这可以给您一个进一步查找的提示。 - Gert Arnold
显示剩余14条评论
2个回答

1
看起来 EF 已经在追踪 user 和 AccessToken。因此,让我们尝试避免获取相同实体的另一个实例,并重用已经被追踪的实例。
尝试
[HttpPost]
public IActionResult DeleteAccessToken([FromBody]User user)
{
    // Requires System.Linq
    if (Context.Users.Any(u => u.Id == user.Id))
    {
        var accessTokenEntity = Context.AccessTokens.Find(user.AccessToken.Id); // Find the entity of the accesstoken (I tried also directly by accessing the navigation property of user entity)
        Context.AccessTokens.Remove(accessTokenEntity);

        // NOTE: You re not saving?
        return Ok();
    }
    else
    {
        return Unauthorized();
    }
}

或者尝试以下方法:

[HttpPost]
public IActionResult DeleteAccessToken([FromBody]User user)
{
    if (Context.Users.Any(u => u.Id == user.Id))
    {
        Context.AccessTokens.Remove(user.AccessToken);
        return Ok();
    }
    else
    {
        return Unauthorized();
    }
}

6
这看起来像是一种解决方法,但没有任何解释问题最初的原因。 - Evk
3
我同意Evk的看法,它关乎解决谜团,而不是回避它。 - Gert Arnold
2
是的,但为什么?在正常使用情况下,这不应该发生在OP代码中。 - Evk
@HristoYankov 第一段代码可以运行,但我不明白为什么会这样,我可以提供更多信息来理解为什么会这样吗?我没有尝试第二段代码,但我猜它可能不会工作,因为“user”只是从Json文本创建的对象,而不是指向数据库条目的引用,我是对的吗? - Mohammed Noureldin
发生的情况是EF正在跟踪user(在方法参数中)。因此,我们重复使用它,而不是获取一个新的跟踪实体(这会导致异常)。为什么EF要跟踪user-我不确定。也许这是EF-Core的特性,或者是您项目中的设置。 - hyankov
显示剩余2条评论

0

尝试替换这一行:

var accessTokenEntity = Context.AccessTokens.Find(userEntity.AccessToken.Id);

至:

var accessTokenEntity = Context.AccessTokens.AsNoTracking().Find(userEntity.AccessToken.Id);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接