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

Posted

技术标签:

【中文标题】ASP.NET MVC - 附加类型为“MODELNAME”的实体失败,因为同一类型的另一个实体已经具有相同的主键值【英文标题】:ASP.NET MVC - Attaching an entity of type 'MODELNAME' failed because another entity of the same type already has the same primary key value 【发布时间】:2014-06-05 18:48:54 【问题描述】:

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

是否有人在我的代码中看到任何错误或了解在编辑模型期间在什么情况下会引发此类错误?

【问题讨论】:

在设置 EntityState 之前,您是否尝试过附加您的实体?由于您的实体来自发布请求,因此不应被当前上下文跟踪,我猜它认为您尝试添加具有现有 ID 的项目 我试过这个,结果完全一样 :( 出于某种原因,上下文认为我正在创建一个新项目,但我只是更新现有项目... 我在抛出错误之前检查了“a”的状态,并且该对象的状态为“已分离”,但调用 db.As.Attach(aViewModel.a) 会抛出完全相同的消息?有什么想法吗? 我刚刚看到您的更新,您是如何设置上下文生命周期范围的?是按要求吗?如果您的两个操作之间的 db 实例相同,则可以解释您的问题,因为您的项目是由 GET 方法加载的(然后由上下文跟踪),并且它可能无法将您的 POST 方法中的那个识别为之前获取的实体。 canUserAccessA() 是直接加载实体还是作为另一个实体的关系加载? 【参考方案1】:

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

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

//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/

【讨论】:

您好 Kaspars,感谢您的意见。 Attach 方法会引发与我的问题中提到的相同的错误。问题是 canUserAccessA() 函数加载实体以及上面注意到的 CodeCaster。但是说我对你关于安全的建议很感兴趣。你能建议我应该怎么做才能防止这种行为吗? 用有关如何防止安全漏洞的附加信息更新了我的答案。【参考方案2】:

问题解决了!

Attach 方法可能对某人有所帮助,但在这种情况下它无济于事,因为在 Edit 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() 一起使用,但这并不重要,因为我可以通过更改查询来实现同样的效果。

希望这对有类似问题的人有所帮助!

【讨论】:

稍微整洁且性能更高:if (db.As.AsNoTracking().Any(x => x.aID == aID && x.UserID==userID)) 注意:您需要using System.Data.Entity; 才能使用AsNoTracking() 在我的情况下,仅更新实体 id 以外的字段工作正常: var entity = context.Find(entity_id); entity.someProperty = newValue; context.Entry(entity).Property(x => x.someProperty).IsModified = true; context.SaveChanges(); 大量帮助。我在 FirstOrDefault() 之前添加了 .AsNoTracking() 并且它起作用了。【参考方案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 来使用以前的实例。

【讨论】:

【参考方案4】:

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

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

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

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

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

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

【讨论】:

【参考方案5】:

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

因此,如果您使用某种存储库进行实体持久性,则可以简单地分离显式加载的实体,然后将绑定的 EntityState 设置为已修改。

示例(抽象)代码:

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;

【讨论】:

这对我有用,虽然我没有费心使用存储库来引用上下文实体状态。【参考方案6】:

有趣的是:

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

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

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

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

【讨论】:

太棒了,这在我需要在断开连接的应用程序中使用自定义连接表更新多对多记录的场景中非常有效。即使从数据库中获取了实体,我也遇到了引用错误等。我使用的是“context.Entry(score).State = System.Data.Entity.EntityState.Modified;”但这终于奏效了!谢谢!! 这行得通。所有其他关于附加和使用 nottracking 的建议都失败了,因为我已经在做 noTracking。感谢您的解决方案。 这对我有用,同时更新了同一工作单元中的父子实体。非常感谢 对于任何人来说,AddOrUpdateSystem.Data.Entity.Migrations 命名空间中的扩展方法。 @Artyomska 不幸的是我不知道。【参考方案7】:

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

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

【讨论】:

【参考方案8】:

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

【讨论】:

【参考方案9】:

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

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
        
                

希望这会有所帮助...

【讨论】:

【参考方案10】:

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

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

首先从 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

【讨论】:

【参考方案11】:

我在本地 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的对象之间的冲突

【讨论】:

@Artjom B 使用相同键加载对象的问题原因,因此首先我们将分离该对象并进行更新以避免具有相同键的两个对象之间发生冲突【参考方案12】:

我设法通过更新状态来解决问题。当您触发 find 或同一记录状态上的任何其他查询操作已更新为 modified 所以我们需要将状态设置为 Detached 然后您可以触发您的更新更改

     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;
            

【讨论】:

【参考方案13】:

试试这个:

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;

【讨论】:

【参考方案14】:

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

【讨论】:

【参考方案15】:

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

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 是西班牙语的地方(寻找第二个答案)

【讨论】:

请小心,仅使用 1 个数据库连接实例,特别是如果您使用实体框架,如果您不这样做,您将收到错误 Entity Framework An entity object cannot be referenced by multiple instances IEntityChangeTracker【参考方案16】:

在获取查询的位置使用AsNoTracking()

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

【讨论】:

【参考方案17】:

我在哪里遇到这个错误

两个方法 A 和 B 在单个控制器中都使用相同的 ApplicationDbContext 实例, 方法A调用方法B
    private ApplicationDbContext db;
    // api methods
    public JsonResult methodA(string id)
        Resource resource = db.Resources.Find(id);
        db.Entry(resource).State = EntityState.Modified;
        db.SaveChanges();
        return methodB()
    

    public JsonResult methodB(string id)
        Resource resource = db.Resources.Find(id);
        db.Entry(resource).State = EntityState.Modified;
        db.SaveChanges();
        return new JsonResult();
    

我将方法 B 更改为使用 using 语句,并且仅依赖本地 db2。 之后:

    private ApplicationDbContext db;    
    // api methods    
    public JsonResult methodA(string id)
        Resource resource = db.Resources.Find(id);
        db.Entry(resource).State = EntityState.Modified;
        db.SaveChanges();
        return methodB()
    

    public JsonResult methodB(string id)
        using (var db2 = new ApplicationDbContext())
        
            Resource resource = db2.Resources.Find(id);
            db2.Entry(resource).State = EntityState.Modified;
            db2.SaveChanges();
        
        return new JsonResult();
    

【讨论】:

【参考方案18】:

您可以使用添加的方法,例如;

_dbContext.Entry(modelclassname).State = EntityState.Added;

但在许多情况下,如果您当时想使用多个模型,这将不起作用,因为实体已经附加到另一个实体。因此,那时您可以使用 ADDOrUpdate Entity Migration 方法,该方法只是将对象从一个对象迁移到另一个对象,因此您不会收到任何错误。

_dbContext.Set<modelclassname>().AddOrUpdate(yourmodel);

【讨论】:

【参考方案19】:

清除所有状态

dbContextGlobalERP.ChangeTracker.Entries().Where(e => e.Entity != null).ToList().ForEach(e => e.State = EntityState.Detached);

【讨论】:

【参考方案20】:

我遇到此错误的原因:

    在查询现有实体时未使用.AsNoTracking()。尤其是在调用辅助函数来检查权限时。 在查询上调用.Include(),然后尝试编辑父项。示例:var ent = repo.Query&lt;Ent&gt;().Include(e=&gt;e.Ent2).First(); ...repo.Edit(e.Ent2); repo.Edit(e); 如果我要编辑嵌套对象,我现在尝试将它们分成单独的查询调用。如果您不能这样做,请将子对象设置为 null 并遍历列表,分离像 this 这样的对象 在Put 网络呼叫中编辑旧实体。新项目已添加到存储库中,因此请修改该项目并将其保存在 super.Put() 中。将引发错误的示例:public void Put(key, newItem) var old = repo.Query&lt;Entity&gt;().Where(e=&gt;Id==key).First(); ... repo.Edit(old); super.Put(key,newItem); ... 多个帮助函数编辑同一个实体。不要将 ID 作为参数传递给每个函数,而是传递对实体的引用。错误已解决!

【讨论】:

【参考方案21】:

就我而言,我确实写过两次相同类型的实体。所以我删除它,一切正常

【讨论】:

以上是关于ASP.NET MVC - 附加类型为“MODELNAME”的实体失败,因为同一类型的另一个实体已经具有相同的主键值的主要内容,如果未能解决你的问题,请参考以下文章

ASP.net core MVC Framework 5 Formdata在附加图像时类型不正确

asp.net mvc框架中 怎样在一个 View 内传递多个 Model,最好有代码,谢谢

ASP.NET-MVC中Entity和Model之间的关系

ASP.Net Core MVC 如何针对强类型模型发布未知数量的字段

在 ASP.NET MVC Core 3.1 中通过 jQuery 为部分视图附加 html 的问题

将 ant 性能分析器附加到 asp.net-mvc 站点失败