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

6

提供一个使用Entity Framework Core和SQL数据库的ASP.NET Core网站应用程序。

当尝试更新数据库中的实体时,绝对简单的操作会引发此异常。这是在生产环境中通过错误报告首次注意到的。

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(string id, [Bind("Group")] EditViewModel model)
{
    if (id != model.Group.Id) return NotFound();

    if (!ModelState.IsValid) return View(model);

    _context.Update(model.Group);
    await _context.SaveChangesAsync();

    return RedirectToAction("Index");
}

在这行代码_context.Update(model.Group);处抛出了异常:

InvalidOperationException: 实体类型“Group”的实例无法跟踪,因为已经跟踪了另一个具有相同键的此类型的实例。对于大多数关键类型,如果未设置关键字(即如果为其类型分配默认值),则添加新实体时将创建唯一的临时关键字值。如果您显式地为新实体设置关键字值,请确保它们不与现有实体或为其他新实体生成的临时值发生冲突。在附加现有实体时,请确保只附加具有给定键值的一个实体实例到上下文中。

显然没有其他实例。当我在该行代码上停下断点并展开_context.Group对象的Results属性时,在我的开发环境中可以重现此异常:

screenshot of expanded Results property of the _context.Group object

很容易理解,展开Results时,它会加载需要更新的实例,这就是为什么会抛出异常。但是正式部署的生产环境呢?

感谢您的帮助!

UPDATE1 Group模型:

public class Group
{
    [Display(Name = "ID")]
    public string Id { get; set; }

    public virtual Country Country { get; set; }

    [Required]
    [Display(Name = "Country")]
    [ForeignKey("Country")]
    public string CountryCode { get; set; }

    [Required]
    [Display(Name = "Name")]
    public string Name { get; set; }
}

更新2 基于@Mithgroth的答案,我能够重写函数_context.Update(),使其每次使用时不需要try-catch:

public interface IEntity 
{
    string Id { get; }
}

public override EntityEntry<TEntity> Update<TEntity>(TEntity entity)
{
    if (entity == null)
    {
        throw new System.ArgumentNullException(nameof(entity));
    }

    try
    {
        return base.Update(entity);
    }
    catch (System.InvalidOperationException)
    {
        var originalEntity = Find(entity.GetType(), ((IEntity)entity).Id);
        Entry(originalEntity).CurrentValues.SetValues(entity);
        return Entry((TEntity)originalEntity);
    }
}

2
这些都是惰性加载的集合。当您展开 _context.Group 时,Entity Framework 将查询存储并使用实体填充内存上下文。然后,当您尝试在调用 Update 时附加非跟踪实体 model.Group 时,它将尝试附加此外部实例,然后会发现与已存在的实体冲突(我假设存在于后备存储(数据库)中)。您是否在请求之间重复使用 Entity Framework 上下文实例? - odyss-jii
1
在控制器中,你不应该接收实体。你应该使用视图模型,并且要么手动映射它们,要么使用映射库来在视图模型和实体之间进行转换。 - Camilo Terevinto
嗨@odyss-jii,是的,这很清楚,但这只是在我调试和扩展结果时枚举它。但为什么问题存在于发布 - 生产中?不应该有任何枚举或断点。而且,据我所知,我正在重用DbContext:services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); - Mark Szabo
嗯,AddDbContext<..> 应该会为每个请求创建一个作用域上下文,所以你的代码应该可以工作。如果你能在开发环境中重现它(不扩展加载整个表的 DbSet),请设置断点并检查 _context.Group.Local.Count 属性是否为 0。还有,model.Group 包含什么。顺便问一下,我们能看到 Group 实体模型(类)吗?特别是导航属性(有没有可能包含其他 Group 对象?) - Ivan Stoev
1
请尝试以下代码:var group = db.Groups.First(g => g.Id == model.Group.Id); db.Entry(group).CurrentValues.SetValues(model.Group); db.SaveChanges(); - Mithgroth
显示剩余3条评论
2个回答

19

请使用以下内容代替:

var group = _context.Group.First(g => g.Id == model.Group.Id);
_context.Entry(group).CurrentValues.SetValues(model.Group); 
await _context.SaveChangesAsync();

异常可能由许多不同的情况引起,但问题在于,您正在尝试更改已经被标记为不同状态的对象的状态。

例如,这将产生相同的异常:

var group = new Group() { Id = model.Id, ... };
db.Update(group);

或者您可能已经分离了N层子项,这是完全可能的。

这可以确保您仅覆盖现有实体的值。


非常感谢你,Mithgroth!我只有一个问题,就是在这种情况下,似乎没有任何东西会导致原始对象发生变化,那么如何确保在其他控制器中,Update() 函数不会出现同样的失败呢?我需要全部修改吗?还是可以重写整个 _context.Update() 函数?谢谢! - Mark Szabo
我不能确定,但我也是通过艰苦的方式学到的。如果您想要,可以使用try catch并回退到这种风格。或者您可以全力以赴,因为我还没有看到执行CurrentValues.SetValues()会有任何副作用。它只是比直接调用“Update”感觉有点尴尬。欢迎任何人以比我更复杂的方式解释 :) - Mithgroth
1
好的,谢谢@Mithgroth!顺便说一下,我成功重写了_context.Update()函数,请查看原问题末尾的UPDATE2 - Mark Szabo
唯一有效的解决方案。 - Farukest

1
感谢大家提供的想法。我已经成功地覆盖了上下文中的函数,这是我认为略微更好的方法,并且使用了定义的模型来检索主键名称,没有出现异常。
另外,这是EntityFramework 3.0 .NET Core。
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using System.Linq;



    public override EntityEntry<TEntity> Update<TEntity>(TEntity entity) where TEntity : class
    {
        if (entity == null)
        {
            throw new System.ArgumentNullException(nameof(entity));
        }

        var type = entity.GetType();
        var et = this.Model.FindEntityType(type);
        var key = et.FindPrimaryKey();

        var keys = new object[key.Properties.Count];
        var x = 0;
        foreach(var keyName in key.Properties)
        {
            var keyProperty = type.GetProperty(keyName.Name, BindingFlags.Public | BindingFlags.Instance);
            keys[x++] = keyProperty.GetValue(entity);
        }

        var originalEntity = Find(type, keys);
        if (Entry(originalEntity).State == EntityState.Modified)
        {
            return base.Update(entity);
        }

        Entry(originalEntity).CurrentValues.SetValues(entity);
        return Entry((TEntity)originalEntity);
    }

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