ASP.NET MVC - 由于另一个相同类型的实体已经具有相同的主键值,因此附加类型为'MODELNAME'的实体失败。

142
总的来说,在POST包装模型并将一个实体的状态更改为“修改”期间,引发了异常。在更改状态之前,状态被设置为“已分离”,但调用Attach()会抛出相同的错误。我正在使用EF6。
请查看下面的代码(模型名称已更改以使其更易于阅读)。

模型

// Wrapper classes
        public class AViewModel
        {
            public A a { get; set; }
            public List<B> b { get; set; }
            public C c { get; set; }
        }   

控制器

        public ActionResult Edit(int? id)
        {
            if (id == null)
            {
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }

            if (!canUserAccessA(id.Value))
                return new HttpStatusCodeResult(HttpStatusCode.Forbidden);

            var aViewModel = new AViewModel();
            aViewModel.A = db.As.Find(id);

            if (aViewModel.Receipt == null)
            {
                return HttpNotFound();
            }

            aViewModel.b = db.Bs.Where(x => x.aID == id.Value).ToList();
            aViewModel.Vendor = db.Cs.Where(x => x.cID == aViewModel.a.cID).FirstOrDefault();

            return View(aViewModel);
        }

[HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Edit(AViewModel aViewModel)
        {
            if (!canUserAccessA(aViewModel.a.aID) || aViewModel.a.UserID != WebSecurity.GetUserId(User.Identity.Name))
                return new HttpStatusCodeResult(HttpStatusCode.Forbidden);

            if (ModelState.IsValid)
            {
                db.Entry(aViewModel.a).State = EntityState.Modified; //THIS IS WHERE THE ERROR IS BEING THROWN
                db.SaveChanges();
                return RedirectToAction("Index");
            }
            return View(aViewModel);
        }

如上所示的线条

db.Entry(aViewModel.a).State = EntityState.Modified;

抛出异常:

  

因为另一个具有相同主键值的实体已经存在,所以无法附加类型为'A'的实体,当使用'Attach'方法或将实体状态设置为'Unchanged'或'Modified'时,如果图中的任何实体具有冲突的键值,则可能会发生这种情况。这可能是因为一些实体是新的,并且尚未收到数据库生成的键值。在这种情况下,请使用“Add”方法或“Added”实体状态来跟踪图形,然后根据需要将非新实体的状态设置为“Unchanged”或“Modified”。

在编辑模型时,是否有人看到我的代码有问题或者了解在什么情况下会引发此类错误?


我已经尝试过这个并且结果完全相同 :( 由于某些原因,上下文认为我正在创建一个新项目,但我只是在更新现有的项目... - Chris Ciszak
在错误被抛出之前,我检查了'a'的状态,这个对象的状态是'Detached',但是调用db.As.Attach(aViewModel.a)却会抛出完全相同的消息?有什么想法吗? - Chris Ciszak
7
我刚看到你的更新,你是怎样设置你的上下文生命周期范围的?它是每个请求都有一个吗?如果在你的两个动作中db实例是相同的,那么这可能解释了你的问题,因为你的物品是由GET方法加载的(然后被上下文跟踪),而它可能无法将POST方法中获取的实体识别为之前获取的实体。 - Réda Mattar
我认为我们可能在正确的轨道上,但我不确定如何检查我的上下文生命周期范围?我在我的控制器中有“protected override void Dispose(bool disposing)”。MVC是否应该在每个请求后自动调用它? - Chris Ciszak
1
canUserAccessA() 方法是直接加载实体还是作为另一个实体的关系? - CodeCaster
显示剩余4条评论
21个回答

172

问题已解决!

Attach方法可能会对某些人有帮助,但在这种情况下并没有帮助,因为文档在编辑GET控制器函数中加载时已经被跟踪。使用Attach方法将抛出完全相同的错误。

我在这里遇到的问题是由函数canUserAccessA()引起的,它在更新对象a的状态之前会先加载A实体。这会破坏被跟踪的实体,并将a对象的状态更改为Detached

解决方案是修改canUserAccessA()函数,使其不跟踪我正在加载的对象。在查询上下文时应调用AsNoTracking()函数。

// User -> Receipt validation
private bool canUserAccessA(int aID)
{
    int userID = WebSecurity.GetUserId(User.Identity.Name);
    int aFound = db.Model.AsNoTracking().Where(x => x.aID == aID && x.UserID==userID).Count();

    return (aFound > 0); //if aFound > 0, then return true, else return false.
}

由于某种原因,我无法在AsNoTracking().Find(aID)一起使用,但这并不重要,因为我可以通过更改查询来实现相同的效果。

希望这能帮助遇到类似问题的任何人!


10
稍微更整洁和高效一些:如果 (db.As.AsNoTracking().Any(x => x.aID == aID && x.UserID==userID)) - Brent
11
注意:您需要使用 using System.Data.Entity; 才能使用 AsNoTracking() - Maxime
在我的情况下,仅更新实体 ID 以外的字段是有效的: var entity = context.Find(entity_id); entity.someProperty = newValue; context.Entry(entity).Property(x => x.someProperty).IsModified = true; context.SaveChanges(); - Anton Lyhin
4
巨大的帮助。我在我的FirstOrDefault()之前添加了.AsNoTracking(),然后它就起作用了。 - coggicc

122

有趣的是:

_dbContext.Set<T>().AddOrUpdate(entityToBeUpdatedWithId);

或者如果您仍然不是通用的:

_dbContext.Set<UserEntity>().AddOrUpdate(entityToBeUpdatedWithId);

看起来顺利解决了我的问题。


1
太棒了,在我的场景中,这完美地解决了我需要在一个断开的应用程序中使用自定义连接表更新多对多记录的问题。即使从数据库中获取了实体,我仍然会出现引用错误等问题。我一直在使用"context.Entry(score).State = System.Data.Entity.EntityState.Modified;",但最终这个方法管用了!谢谢!! - firecape
6
这个方法有效。所有其他关于附加和使用notracking的建议都失败了,因为我已经在使用noTracking了。感谢您提供的解决方案。 - Khainestar
3
同一工作单元中更新父实体和子实体时,这对我很有帮助。非常感谢。 - Ian
60
对于任何需要查找的人,AddOrUpdate是在System.Data.Entity.Migrations命名空间中的一个扩展方法。 - Nick
1
@guneysus,我有同样的问题,你的建议起作用了。但是我的问题有点不同。如果你能看一下这个问题,那就好了。 - Avi Kenjale
显示剩余4条评论

18

看起来你试图修改的实体没有被正确地跟踪,因此没有被识别为已编辑,而是被添加了。

尝试不直接设置状态,而是进行以下操作:

//db.Entry(aViewModel.a).State = EntityState.Modified;
db.As.Attach(aViewModel.a); 
db.SaveChanges();

此外,我想警告您,您的代码可能存在安全漏洞。 如果您直接在视图模型中使用实体,则有人可以通过在提交的表单中添加正确命名的字段来修改实体的内容。例如,如果用户添加了名称为“A.FirstName”的输入框,而实体包含该字段,则该值将绑定到视图模型并保存到数据库中,即使用户在应用程序的正常操作中不被允许更改也是如此。

更新:

为了解决上述安全漏洞,您不应将域模型暴露为视图模型,而应使用单独的视图模型。然后,您的操作将接收视图模型,您可以使用一些映射工具(如AutoMapper)将其映射回域模型。这将使您免受用户修改敏感数据的威胁。

以下是详细说明:

http://www.stevefenton.co.uk/Content/Blog/Date/201303/Blog/Why-You-Never-Expose-Your-Domain-Model-As-Your-MVC-Model/


3
嗨,Kaspars,感谢你的回复。Attach方法会抛出与我问题中提到的相同的错误。问题在于canUserAccessA()函数不仅加载实体,而且如CodeCaster上面所注意到的那样也会加载代码。但我对你关于安全方面的建议非常感兴趣。你能建议我该怎么做来防止这种行为吗? - Chris Ciszak
更新了我的答案,并提供了有关如何防止安全漏洞的额外信息。 - Kaspars Ozols

16

试一试:

var local = yourDbContext.Set<YourModel>()
                         .Local
                         .FirstOrDefault(f => f.Id == yourModel.Id);
if (local != null)
{
  yourDbContext.Entry(local).State = EntityState.Detached;
}
yourDbContext.Entry(applicationModel).State = EntityState.Modified;

13

对我来说,本地副本是问题的根源。这样解决了它。

var local = context.Set<Contact>().Local.FirstOrDefault(c => c.ContactId == contact.ContactId);
                if (local != null)
                {
                    context.Entry(local).State = EntityState.Detached;
                }

10

我的情况是,我没有从我的MVC应用程序直接访问EF上下文。

因此,如果您正在使用某种存储库进行实体持久化,那么明确分离加载的实体并将绑定的EntityState设置为Modified可能是合适的。

示例(抽象)代码:

MVC

public ActionResult(A a)
{
  A aa = repo.Find(...);
  // some logic
  repo.Detach(aa);
  repo.Update(a);
}

仓库

void Update(A a)
{
   context.Entry(a).EntityState = EntityState.Modified;
   context.SaveChanges();
}

void Detach(A a)
{
   context.Entry(a).EntityState = EntityState.Detached;
}

这对我有用,尽管我没有使用存储库来引用上下文实体状态。 - Eckert

7

在获取查询结果时,请使用AsNoTracking()

  var result = dbcontext.YourModel.AsNoTracking().Where(x => x.aID == aID && x.UserID==userID).Count();

4
我之所以添加这个答案,是因为问题是基于更复杂的数据模式解释的,我觉得在这里很难理解。
我创建了一个相当简单的应用程序。这个错误发生在Edit POST操作中。该操作接受ViewModel作为输入参数。使用ViewModel的原因是在保存记录之前进行一些计算。
一旦操作通过验证,例如if(ModelState.IsValid),我的错误是将ViewModel中的值投影到完全新的实体实例中。我认为我必须创建一个新实例来存储更新后的数据,然后保存这样的实例。
我后来意识到,我必须从数据库中读取记录。
Student student = db.Students.Find(s => s.StudentID == ViewModel.StudentID);

并更新了这个对象。现在一切都正常运作。


3

虽然我觉得自己有点傻,但我想分享一下我的经验。

我正在使用存储库模式,并将repo实例注入到我的控制器中。具体的存储库会实例化我的ModelContext(DbContext),它的寿命是存储库的生命周期,它是IDisposable并由控制器处理。

对我来说问题在于我的实体上有一个修改时间戳和行版本,所以我首先获取它们以与传入的标头进行比较。当然,这样加载并跟踪了随后被更新的实体。

解决方法很简单,只需将存储库从在构造函数中新建上下文更改为具有以下方法:

    private DbContext GetDbContext()
    {
        return this.GetDbContext(false);
    }


    protected virtual DbContext GetDbContext(bool canUseCachedContext)
    {
        if (_dbContext != null)
        {
            if (canUseCachedContext)
            {
                return _dbContext;
            }
            else
            {
                _dbContext.Dispose();
            }
        }

        _dbContext = new ModelContext();

        return _dbContext;
    }

    #region IDisposable Members

    public void Dispose()
    {
        this.Dispose(true);
    }

    protected virtual void Dispose(bool isDisposing)
    {
        if (!_isDisposed)
        {
            if (isDisposing)
            {
                // Clear down managed resources.

                if (_dbContext != null)
                    _dbContext.Dispose();
            }

            _isDisposed = true;
        }
    }

    #endregion

这使得存储库方法能够通过调用GetDbContext来在每次使用时更新其上下文实例,或者如果需要,通过指定true来使用先前的实例。

3

我曾经遇到过局部变量的问题,解决方法是这样的:

将它分离出来就可以了:
if (ModelState.IsValid)
{
    var old = db.Channel.Find(channel.Id);
    if (Request.Files.Count > 0)
    {
        HttpPostedFileBase objFiles = Request.Files[0];
        using (var binaryReader = new BinaryReader(objFiles.InputStream))
        {
            channel.GateImage = binaryReader.ReadBytes(objFiles.ContentLength);
        }

    }
    else
        channel.GateImage = old.GateImage;
    var cat = db.Category.Find(CatID);
    if (cat != null)
        channel.Category = cat;
    db.Entry(old).State = EntityState.Detached; // just added this line
    db.Entry(channel).State = EntityState.Modified;
    await db.SaveChangesAsync();
    return RedirectToAction("Index");
}
return View(channel);

问题原因是加载了具有相同键的对象,因此我们首先将分离该对象并进行更新,以避免两个具有相同键的对象之间的冲突。


问题原因是加载了具有相同键的对象,因此我们首先将分离该对象并进行更新以避免两个具有相同键的对象之间的冲突。 - lvl4fi4

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