首页 文章

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

提问于
浏览
98

简而言之,在POSTing包装器模型期间抛出异常并将一个条目的状态更改为“已修改” . 在更改状态之前,状态设置为'Detached'但调用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”的实体失败,因为同一类型的另一个实体已具有相同的主键值 . 如果图中的任何实体具有冲突的键值,则在使用“附加”方法或将实体的状态设置为“未更改”或“已修改”时,可能会发生这种情况 . 这可能是因为某些实体是新的并且尚未收到数据库生成的键值 . 在这种情况下,使用“添加”方法或“已添加”实体状态来跟踪图形,然后根据需要将非新实体的状态设置为“未更改”或“已修改” .

有没有人在我的代码中看到任何错误或了解在编辑模型期间会在什么情况下抛出这样的错误?

15 回答

  • 2

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

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

    试试这个:

    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;
    
  • 128

    您尝试修改的实体似乎未被正确跟踪,因此未被识别为已编辑,而是添加 .

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

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

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

    Update:

    为了克服前面提到的安全漏洞,您不应该将域模型公开为viewmodel,而是使用单独的viewmodel . 然后你的动作会收到viewmodel,你可以使用像AutoMapper这样的映射工具将其映射回域模型 . 这样可以避免用户修改敏感数据 .

    这是扩展说明:

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

  • 1

    问题解决了!

    Attach 方法可能有助于某人,但在这种情况下它无济于事,因为在编辑GET控制器功能中加载文档时已经跟踪了该文档 . Attach会抛出完全相同的错误 .

    我在这里遇到的问题是由函数 canUserAccessA() 引起的,它在更新对象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.
    }
    

    出于某种原因,我不能将 .Find(aID)AsNoTracking() 一起使用,但这并不重要,因为我可以通过更改查询来实现相同目的 .

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

  • 3

    有趣的是:

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

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

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

    似乎顺利解决了我的问题 .

  • 1

    我的情况是我没有从我的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;
    }
    
  • 1

    我以为我会在这个上分享我的经验,尽管我觉得有点傻到没有早点意识到 .

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

    对我来说问题是我的实体上有一个修改过的印章和行版本,所以我先得到它们以便与入站 Headers 进行比较 . 当然,这会加载并跟踪随后更新的实体 .

    修复只是在构造函数中将存储库从一个上下文更改为一次有以下方法:

    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则使用先前的实例 .

  • 8

    我之所以添加这个答案只是因为问题是基于更复杂的数据模式来解释的,我发现这里很难理解 .

    我创建了一个相当简单的应用程Edit POST操作中发生此错误 . 该操作接受ViewModel作为输入参数 . 使用ViewModel的原因是在保存记录之前进行一些计算 .

    一旦操作通过验证(例如 if(ModelState.IsValid) ),我的错误就是将ViewModel中的值投影到一个全新的Entity实例中 . 我以为我必须创建一个新实例来存储更新的数据,然后保存这样的实例 .

    我后来意识到的是我必须从数据库中读取记录:

    Student student = db.Students.Find(s => s.StudentID == ViewModel.StudentID);
    

    并更新了这个对象 . 现在一切都有效 .

  • 5

    与Luke Puplett所说的相似,问题可能是由于没有正确处理或创建您的上下文 .

    在我的例子中,我有一个类接受了一个名为 ContextService 的上下文:

    public class ContextService : IDisposable
    {
        private Context _context;
    
        public void Dispose()
        {
            _context.Dispose();
        }
        public ContextService(Context context)
        {
            _context = context;
        }
    //... do stuff with the context
    

    我的上下文服务有一个函数,它使用实例化的实体对象更新实体:

    public void UpdateEntity(MyEntity myEntity, ICollection<int> ids)
            {
                var item = _context.Entry(myEntity);
                item.State = EntityState.Modified;
                item.Collection(x => x.RelatedEntities).Load();
                myEntity.RelatedEntities.Clear();
                foreach (var id in ids)
                {
                    myEntity.RelatedEntities.Add(_context.RelatedEntities.Find(id));
                }
                _context.SaveChanges();
            }
    

    所有这一切都很好,我的控制器初始化服务是问题所在 . 我的控制器最初看起来像这样:

    private static NotificationService _service = 
            new NotificationService(new NotificationContext());
        public void Dispose()
        {
        }
    

    我把它改成了这个错误消失了:

    private static NotificationService _service;
        public TemplateController()
        {
            _service = new NotificationService(new NotificationContext());
        }
        public void Dispose()
        {
            _service.Dispose();
        }
    
  • 1

    ViewModelEntityModel 映射期间也可以看到此问题(通过使用 AutoMapper 等)并尝试包含 context.Entry().Statecontext.SaveChanges() 这样的使用块如下所示将解决问题 . 请记住, context.SaveChanges() 方法使用了两次而不是在 if-block 之后使用,因为它也必须在使用块中 .

    public void Save(YourEntity entity)
    {
        if (entity.Id == 0)
        {
            context.YourEntity.Add(entity);
            context.SaveChanges();
        }
        else
        {
            using (var context = new YourDbContext())
            {
                context.Entry(entity).State = EntityState.Modified;
                context.SaveChanges(); //Must be in using block
            }
        }            
    }
    

    希望这可以帮助...

  • 13

    这是我在类似情况下所做的 .

    这种情况意味着在上下文中已存在相同的实体 . 以下内容可以提供帮助

    如果实体位于上下文中,请首先从ChangeTracker进行检查

    var trackedEntries=GetContext().ChangeTracker.Entries<YourEntityType>().ToList();
    
    var isAlreadyTracked =
                        trackedEntries.Any(trackedItem => trackedItem.Entity.Id ==myEntityToSave.Id);
    

    如果它存在

    if (isAlreadyTracked)
                {
                    myEntityToSave= trackedEntries.First(trackedItem => trackedItem.Entity.Id == myEntityToSave.Id).Entity;
                } 
    
    else
    {
    //Attach or Modify depending on your needs
    }
    
  • 1

    我有本地var的这个问题,我只是像这样分离它:

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

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

  • 1

    我通过更新状态来修复问题 . 当您在同一记录上触发查找或任何其他查询操作时,已更新修改了状态,因此我们需要将状态设置为已分离,然后您可以触发更新更改

    ActivityEntity activity = new ActivityEntity();
          activity.name="vv";
        activity.ID = 22 ; //sample id
       var savedActivity = context.Activities.Find(22);
    
                if (savedActivity!=null)
                {
                    context.Entry(savedActivity).State = EntityState.Detached;
                    context.SaveChanges();
    
                    activity.age= savedActivity.age;
                    activity.marks= savedActivity.marks; 
    
                    context.Entry(activity).State = EntityState.Modified;
                    context.SaveChanges();
                    return activity.ID;
                }
    
  • 0

    我用“使用”块解决了这个问题

    using (SqlConnection conn = new SqlConnection(connectionString))
    
        {
    
           // stuff to do with data base
        }
    
        // or if you are using entity framework 
        using (DataBaseEntity data = new DataBaseEntity)
    {
    
        }
    

    这是我得到的想法https://social.msdn.microsoft.com/Forums/sqlserver/es-ES/b4b350ba-b0d5-464d-8656-8c117d55b2af/problema-al-modificar-en-entity-framework?forum=vcses是西班牙语(寻找第二个答案)

  • 10

    我有一个类似的问题,在探测2-3天后发现“.AsNoTracking”应该被移除,因为EF不跟踪更改并假设除非附加了一个对象,否则没有任何变化 . 此外,如果我们不使用.AsNoTracking,EF会自动知道要保存/更新的对象,因此不需要使用Attach / Added .

相关问题