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


121

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

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


在设置之前,您是否尝试过附加实体EntityState?当你的实体来自POST请求,它不应该由当前上下文跟踪,我猜测它认为你试图与现有的ID添加一个项目
香港地产建设商会感澈

我尝试过这个,结果是完全相同的:(由于某种原因,上下文认为我正在创建一个新项目,但是我只是在更新现有项目...
Chris Ciszak 2014年

我在引发错误之前检查了'a'的状态,并且该对象的状态为'Detached',但是调用db.As.Attach(aViewModel.a)会抛出完全相同的消息吗?有任何想法吗?
克里斯·西扎克

5
我刚刚看到您的更新,您如何设置上下文生存期范围?是每个请求吗?如果db您的两个操作之间的实例相同,则可以解释您的问题,因为您的商品是通过GET方法加载的(然后由上下文跟踪),并且可能无法将POST方法中的实例识别为之前获取的实体。
REDA感澈

1
是否canUserAccessA()直接或作为另一entitiy的关系加载实体?
CodeCaster

Answers:


154

问题解决了!

Attach该方法可能会对某人有所帮助,但在这种情况下将无济于事,因为在将文档加载到Edit GET控制器功能中时已对其进行了跟踪。附加将引发完全相同的错误。

我在这里遇到的问题是由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()但这并不重要,因为我可以通过更改查询来实现相同目的。

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


10
稍微更整洁,性能更高:if(db.As.AsNoTracking()。Any(x => x.aID == aID && x.UserID == userID))
布伦特(Brent

11
注意:您需要using System.Data.Entity;使用AsNoTracking()
Maxime 2015年

在我的情况下,仅更新除实体ID外的所有字段都可以正常工作:var entity = context.Find(entity_id); entity.someProperty = newValue; context.Entry(entity).Property(x => x.someProperty).IsModified = true; context.SaveChanges();
安东·莱因

3
大量的帮助。我在FirstOrDefault()之前添加了.AsNoTracking(),并且可以正常工作。
coggicc '16

109

有趣的是:

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

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

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

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


1
令人惊讶的是,在我需要使用断开连接的应用程序中的自定义联接表来更新多对多记录的情况下,这非常完美。即使从数据库中获取了实体,我也遇到了引用错误,等等。我使用的是“ context.Entry(score).State = System.Data.Entity.EntityState.Modified;”。但这终于奏效了!谢谢!!
firecape

5
这可行。关于附加和使用notracking的所有其他建议都失败了,因为我已经在进行noTracking。感谢您的解决方案。
Khainestar '16

3
这对我有用,同时在同一工作单元中更新父级和子级实体。非常感谢
Ian

55
对于任何人来说,AddOrUpdate都是System.Data.Entity.Migrations名称空间中的扩展方法。
尼克

1
@Artyomska不幸的是我不知道。
guneysus

15

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

代替直接设置状态,请尝试执行以下操作:

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

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

更新:

要克服前面提到的安全漏洞,请不要将域模型公开为视图模型,而应使用单独的视图模型。然后,您的操作将收到viewmodel,您可以使用某些映射工具(例如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。但是,我对您关于安全性的建议非常感兴趣。你能建议我该怎么做以防止这种行为吗?
克里斯·西萨克

使用有关如何防止安全漏洞的其他信息更新了我的答案。
Kaspars Ozols

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;

11

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

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

3

我以为我会分享这一经验,尽管我对没有早一点意识到而感到愚蠢。

我正在使用存储库模式,并将回购实例插入到控制器中。具体的存储库实例化了我的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来使用先前的实例。


2

我之所以添加此答案,仅是因为该问题是根据更复杂的数据模式进行说明的,而在这里我很难理解。

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

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

后来我意识到,我不得不从数据库中读取记录:

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

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


2

我在使用本地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问题使用相同密钥加载对象的原因,因此首先我们将分离该对象并进行更新,以避免具有相同密钥的两个对象之间发生冲突
lvl4fi4

2

在探查2-3天后发现“ .AsNoTracking”应删除,因为EF不会跟踪更改,并假设没有任何更改,除非已附加对象,所以我遇到了类似的问题。另外,如果我们不使用.AsNoTracking,则EF会自动知道要保存/更新的对象,因此无需使用“附加/添加”。



2

我在哪里遇到此错误

  • 单个控制器中的两种方法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();
    }

1

与卢克·普普利特(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
        }
    }            
}

希望这可以帮助...


1

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

这种情况意味着上下文中已经存在相同的实体。

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

我要通过更新状态来解决此问题。当您触发find或对同一记录状态的任何其他查询操作已被修改并进行了更新,因此我们需要将状态设置为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;
            }

1

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

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个数据库conexion实例,特别是如果您使用实体框架,如果您不这样做,则会收到错误Entity Framework一个实体对象不能被IEntityChangeTracker的多个实例引用
Suzume,

1

您可以使用类似的添加方法;

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

但是在很多情况下,如果您当时想使用多个模型,那么该方法将无法正常工作,因为实体已经连接到了另一个实体。因此,那时您可以使用ADDOrUpdate实体迁移方法,该方法仅将对象从一个对象迁移到另一个对象,因此不会出现任何错误。

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

0

清除所有状态

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.