ASP.NET MVC - Collegamento di un'entità di tipo "MODELNAME" non riuscito perché un'altra entità dello stesso tipo ha già lo stesso valore di chiave primaria


122

In poche parole, l'eccezione viene generata durante il POST del modello wrapper e la modifica dello stato di una voce in "Modificato". Prima di modificare lo stato, lo stato è impostato su "Detached" ma la chiamata di Attach () genera lo stesso errore. Sto usando EF6.

Si prega di trovare il mio codice di seguito (i nomi dei modelli sono stati modificati per facilitarne la lettura)

Modello

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

controllore

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

Come mostrato sopra la riga

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

genera un'eccezione:

Il collegamento di un'entità di tipo "A" non è riuscito perché un'altra entità dello stesso tipo ha già lo stesso valore di chiave primaria. Ciò può accadere quando si utilizza il metodo "Allega" o si imposta lo stato di un'entità su "Invariato" o "Modificato" se le entità nel grafico hanno valori chiave in conflitto. Ciò può essere dovuto al fatto che alcune entità sono nuove e non hanno ancora ricevuto i valori chiave generati dal database. In questo caso, utilizza il metodo "Aggiungi" o lo stato dell'entità "Aggiunto" per tenere traccia del grafico, quindi imposta lo stato delle entità non nuove su "Non modificato" o "Modificato", a seconda dei casi.

Qualcuno vede qualcosa di sbagliato nel mio codice o capisce in quali circostanze genererebbe un tale errore durante la modifica di un modello?


Hai provato ad allegare la tua entità prima di impostare il EntityState? Poiché la tua entità proviene da una richiesta di post, non dovrebbe essere tracciata dal contesto corrente, immagino che consideri che provi ad aggiungere un elemento con un ID esistente
Réda Mattar

Ho provato questo e il risultato è esattamente lo stesso :( Per qualche motivo il contesto pensa che sto creando un nuovo oggetto, ma sto solo aggiornando quello esistente ...
Chris Ciszak

Controllo lo stato di 'a' prima che venga generato l'errore e lo stato di questo oggetto è 'Detached' ma la chiamata db.As.Attach (aViewModel.a) genera esattamente lo stesso messaggio? Qualche idea?
Chris Ciszak

5
Ho appena visto il tuo aggiornamento, come hai impostato il tuo ambito di vita del contesto? È per richiesta? Se ladb istanza è la stessa tra le tue due azioni, può spiegare il tuo problema, poiché il tuo articolo viene caricato dal metodo GET (quindi tracciato dal contesto) e potrebbe non riconoscere quello nel tuo metodo POST come l'entità recuperata prima .
Réda Mattar

1
Non canUserAccessA()caricare l'entità direttamente o come un rapporto di un altro entitiy?
CodeCaster

Risposte:


155

Problema risolto!

AttachIl metodo potrebbe potenzialmente aiutare qualcuno, ma non sarebbe d'aiuto in questa situazione poiché il documento era già stato tracciato mentre veniva caricato nella funzione Modifica controller GET. Allegare genererebbe esattamente lo stesso errore.

Il problema che ho riscontrato qui è stato causato dalla funzione canUserAccessA()che carica l'entità A prima di aggiornare lo stato dell'oggetto a. Questo stava rovinando l'entità tracciata e stava cambiando lo stato di un oggetto in Detached.

La soluzione era modificare in canUserAccessA()modo che l'oggetto che stavo caricando non venisse tracciato. La funzione AsNoTracking()dovrebbe essere chiamata durante l'interrogazione del contesto.

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

Per qualche motivo non potrei usare .Find(aID)con AsNoTracking()ma non importa in quanto potrei ottenere lo stesso cambiando la query.

Spero che questo possa aiutare chiunque abbia problemi simili!


10
leggermente più ordinato e più performante: if (db.As.AsNoTracking (). Any (x => x.aID == aID && x.UserID == userID))
Brent

11
Nota: è necessario using System.Data.Entity;utilizzare AsNoTracking().
Maxime

Nel mio caso l'aggiornamento solo dei campi tranne l'ID entità ha funzionato bene: var entity = context.Find (entity_id); entity.someProperty = newValue; context.Entry (entity) .Property (x => x.someProperty) .IsModified = true; context.SaveChanges ();
Anton Lyhin

3
Aiuto enorme. Ho aggiunto .AsNoTracking () prima del mio FirstOrDefault () e ha funzionato.
coggicc

110

È interessante notare che:

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

O se ancora non sei generico:

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

sembra aver risolto il mio problema senza problemi.


1
Incredibile, ha funzionato perfettamente nel mio scenario in cui avevo bisogno di aggiornare i record in molti a molti con una tabella di join personalizzata in un'app disconnessa. Anche con l'entità acquisita dal database, ricevevo errori referenziali, ecc. Stavo usando "context.Entry (score) .State = System.Data.Entity.EntityState.Modified;" ma finalmente ha funzionato! Grazie!!
petardo

5
Funziona. Tutti gli altri suggerimenti su come allegare e utilizzare notracking non sono riusciti perché stavo già facendo noTracking. Grazie per la soluzione.
Khainestar

3
Questo ha funzionato per me durante l'aggiornamento delle entità genitore e figlio all'interno della stessa unità di lavoro . molte grazie
Ian

55
Per chiunque cerchi, AddOrUpdateè un metodo di estensione nello System.Data.Entity.Migrationsspazio dei nomi.
Nick

1
@Artyomska Purtroppo non lo so.
guneysus

15

Sembra che l'entità che stai tentando di modificare non venga tracciata correttamente e quindi non sia riconosciuta come modificata, ma aggiunta.

Invece di impostare direttamente lo stato, prova a fare quanto segue:

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

Inoltre, vorrei avvisarti che il tuo codice contiene una potenziale vulnerabilità di sicurezza. Se stai utilizzando l'entità direttamente nel tuo modello di visualizzazione, rischi che qualcuno possa modificare il contenuto dell'entità aggiungendo campi con nome corretto nel modulo inviato. Ad esempio, se l'utente ha aggiunto la casella di input con il nome "A.FirstName" e l'entità conteneva tale campo, il valore sarebbe associato a viewmodel e salvato nel database anche se all'utente non sarebbe stato consentito di modificarlo durante il normale funzionamento dell'applicazione .

Aggiornare:

Per superare la vulnerabilità di sicurezza menzionata in precedenza, non dovresti mai esporre il tuo modello di dominio come viewmodel ma utilizzare invece viewmodel separato. Quindi la tua azione riceverebbe viewmodel che potresti mappare di nuovo al modello di dominio utilizzando uno strumento di mappatura come AutoMapper. Ciò ti manterrebbe al sicuro dall'utente che modifica i dati sensibili.

Ecco una spiegazione estesa:

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


3
Ciao Kaspars, grazie per l'input. Il metodo Attach genera gli stessi errori menzionati nella mia domanda. Il problema è che la funzione canUserAccessA () carica l'entità così come CodeCaster notato sopra. Ma dicendo che sono molto interessato al tuo suggerimento per quanto riguarda la sicurezza. Potreste suggerirmi cosa dovrei fare per prevenire tale comportamento?
Chris Ciszak

Aggiornata la mia risposta con ulteriori informazioni su come prevenire la vulnerabilità della sicurezza.
Kaspars Ozols

13

Prova questo:

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

per me la copia locale era la fonte del problema. questo lo ha risolto

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

10

Il mio caso era che non avevo accesso diretto al contesto EF dalla mia app MVC.

Quindi, se si utilizza un qualche tipo di repository per la persistenza dell'entità, potrebbe essere appropriato scollegare semplicemente l'entità caricata in modo esplicito e quindi impostare EntityState associato su Modified.

Codice (astratto) di esempio:

MVC

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

deposito

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

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

Questo ha funzionato per me, anche se non mi sono preoccupato di utilizzare un repository per fare riferimento agli stati dell'entità contesto.
Eckert

3

Ho pensato di condividere la mia esperienza su questo, anche se mi sento un po 'sciocco per non averlo realizzato prima.

Sto usando il pattern del repository con le istanze del repository iniettate nei miei controller. I repository concreti istanziano il mio ModelContext (DbContext) che dura per tutta la vita del repository, che èIDisposable ed eliminato dal controller.

Il problema per me era che avevo un timbro modificato e una versione di riga sulle mie entità, quindi le stavo ottenendo per prime per confrontarle con le intestazioni in entrata. Ovviamente, questo ha caricato e monitorato l'entità che è stata successivamente aggiornata.

La soluzione consisteva semplicemente nel cambiare il repository dall'aggiornamento di un contesto una volta nel costruttore ai seguenti metodi:

    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

Ciò consente ai metodi del repository di rinnovare la loro istanza di contesto a ogni utilizzo chiamando GetDbContext, o utilizzare un'istanza precedente se lo desiderano specificando true.


2

Ho aggiunto questa risposta solo perché il problema è spiegato in base a un modello di dati più complesso e ho trovato difficile capire qui.

Ho creato un'applicazione abbastanza semplice. Questo errore si è verificato all'interno dell'azione Modifica POST. L'azione ha accettato ViewModel come parametro di input. Il motivo per utilizzare ViewModel era eseguire alcuni calcoli prima che il record fosse salvato.

Una volta che l'azione è passata attraverso la convalida come if(ModelState.IsValid) , il mio errore è stato quello di proiettare i valori da ViewModel in un'istanza completamente nuova di Entity. Ho pensato di dover creare una nuova istanza per memorizzare i dati aggiornati e quindi ho salvato tale istanza.

Quello che avevo capito in seguito era che dovevo leggere il record dal database:

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

e aggiornato questo oggetto. Adesso funziona tutto.


2

Ho avuto questo problema con la var locale e l'ho staccato in questo modo:

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

Cause del problema di oggetti caricati con la stessa chiave, quindi prima staccheremo quell'oggetto e faremo l'aggiornamento per evitare conflitti tra due oggetti con la stessa chiave


@Artjom B Problemi con gli oggetti caricati con la stessa chiave, quindi prima staccheremo quell'oggetto e faremo l'aggiornamento per evitare conflitti tra due oggetti con la stessa chiave
lvl4fi4

2

Ho avuto un problema simile, dopo aver sondato per 2-3 giorni ho trovato ".AsNoTracking" dovrebbe essere rimosso poiché EF non tiene traccia delle modifiche e presume che non ci siano modifiche a meno che non sia collegato un oggetto. Inoltre, se non utilizziamo .AsNoTracking, EF sa automaticamente quale oggetto salvare / aggiornare, quindi non è necessario utilizzare Allega / Aggiunto.


2

Usa AsNoTracking()dove stai ricevendo la tua domanda.

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

2

Ho riscontrato questo errore dove

  • due metodi, A e B, in un singolo controller utilizzavano entrambi la stessa istanza di un ApplicationDbContext, e
  • metodo A chiamato metodo 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();
    }

Ho cambiato il metodo B per avere un'istruzione using e fare affidamento solo sul db2 locale . Dopo:

    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

Simile a quello che sta dicendo Luke Puplett, il problema può essere causato da una non corretta disposizione o creazione del contesto.

Nel mio caso, avevo una classe che accettava un contesto chiamato ContextService:

public class ContextService : IDisposable
{
    private Context _context;

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

Il mio servizio di contesto aveva una funzione che aggiorna un'entità utilizzando un oggetto entità istanziato:

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

Tutto questo andava bene, il mio controller in cui ho inizializzato il servizio era il problema. Il mio controller originariamente aveva questo aspetto:

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

L'ho cambiato in questo e l'errore è andato via:

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

1

Questo problema può verificarsi anche durante ViewModelalla EntityModelmappatura (utilizzando AutoMapper, etc.) e cercando di includere context.Entry().Statee context.SaveChanges()ad un blocco utilizzando come mostrato sotto risolverebbe il problema. Tieni presente che il context.SaveChanges()metodo viene utilizzato due volte invece di essere utilizzato subito dopo if-blockpoiché deve essere utilizzato anche in blocco.

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

Spero che questo ti aiuti...


1

Ecco cosa ho fatto nel caso simile.

Questa situazione significa che la stessa entità è già esistita nel contesto, quindi seguire può aiutare

Prima controlla da ChangeTracker se l'entità è nel contesto

var trackedEntries=GetContext().ChangeTracker.Entries<YourEntityType>().ToList();

var isAlreadyTracked =
                    trackedEntries.Any(trackedItem => trackedItem.Entity.Id ==myEntityToSave.Id);

Se esiste

  if (isAlreadyTracked)
            {
                myEntityToSave= trackedEntries.First(trackedItem => trackedItem.Entity.Id == myEntityToSave.Id).Entity;
            } 

else
{
//Attach or Modify depending on your needs
}

1

riesco a risolvere il problema aggiornando lo stato. quando si attiva la ricerca o qualsiasi altra operazione di query sullo stesso stato record è stata aggiornata con modificato quindi è necessario impostare lo stato su Scollegato, quindi è possibile attivare la modifica dell'aggiornamento

     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

Risolvo questo problema con un blocco "using"

using (SqlConnection conn = new SqlConnection(connectionString))

    {

       // stuff to do with data base
    }

    // or if you are using entity framework 
    using (DataBaseEntity data = new DataBaseEntity)
{

    }

Qui è dove ho preso l'idea https://social.msdn.microsoft.com/Forums/sqlserver/es-ES/b4b350ba-b0d5-464d-8656-8c117d55b2af/problema-al-modificar-en-entity-framework?forum = vcses è in spagnolo (cerca la seconda risposta)


fai solo attenzione e usa solo 1 istanza di database conexion, specialmente se stai usando entity framework se non lo fai otterrai l'errore Entity Framework Un oggetto entità non può essere referenziato da più istanze di IEntityChangeTracker
Suzume

1

puoi usare metodi aggiunti come;

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

ma in molti casi, se si desidera utilizzare più di un modello in quel momento, ciò non funzionerà perché l'entità è già collegata a un'altra entità. Quindi, in quel momento puoi utilizzare il metodo ADDOrUpdate Entity Migration che migra semplicemente gli oggetti da uno all'altro e di conseguenza non otterrai alcun errore.

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

0

Cancella tutto lo stato

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.