ASP.NET MVC - dołączanie jednostki typu „MODELNAME” nie powiodło się, ponieważ inna jednostka tego samego typu ma już tę samą wartość klucza podstawowego

122

W skrócie wyjątek jest generowany podczas POST-owania modelu otoki i zmiany stanu jednego wpisu na „Zmodyfikowany”. Przed zmianą stanu stan jest ustawiony na „Odłączony”, ale wywołanie metody Attach () powoduje zgłoszenie tego samego błędu. Używam EF6.

Proszę znaleźć mój kod poniżej (nazwy modeli zostały zmienione, aby były bardziej czytelne)

Model

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

Kontroler

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

Jak pokazano powyżej

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

zgłasza wyjątek:

Dołączanie jednostki typu „A” nie powiodło się, ponieważ inna jednostka tego samego typu ma już tę samą wartość klucza podstawowego. Może się to zdarzyć, gdy używasz metody „Dołącz” lub ustawiasz stan jednostki na „Niezmieniony” lub „Zmodyfikowany”, jeśli jakiekolwiek elementy na wykresie mają sprzeczne wartości kluczowe. Może to być spowodowane tym, że niektóre jednostki są nowe i nie otrzymały jeszcze wartości kluczy wygenerowanych przez bazę danych. W takim przypadku użyj metody „Dodaj” lub stanu jednostki „Dodano”, aby prześledzić wykres, a następnie ustaw stan elementów, które nie są nowe, odpowiednio na „Niezmieniony” lub „Zmodyfikowany”.

Czy ktoś widzi coś złego w moim kodzie lub rozumie, w jakich okolicznościach spowodowałby taki błąd podczas edycji modelu?

Chris Ciszak
źródło
Czy próbowałeś dołączyć swój byt przed ustawieniem EntityState? Ponieważ Twój podmiot pochodzi z żądania posta, nie powinien być śledzony przez bieżący kontekst, myślę, że uważa, że ​​próbujesz dodać element z istniejącym identyfikatorem
Réda Mattar
Wypróbowałem ten i wynik jest dokładnie taki sam :( Z jakiegoś powodu kontekst uważa, że ​​tworzę nowy element, ale po prostu aktualizuję istniejący ...
Chris Ciszak
Sprawdzam stan „a” przed wyrzuceniem błędu, a stan tego obiektu to „Odłączony”, ale wywołanie db.As.Attach (aViewModel.a) generuje dokładnie ten sam komunikat? Jakieś pomysły?
Chris Ciszak
5
Właśnie zobaczyłem twoją aktualizację, jak skonfigurowałeś zakres czasu życia kontekstu? Czy to na żądanie? Jeśli dbinstancja jest taka sama między twoimi dwiema akcjami, może to wyjaśnić twój problem, ponieważ twój przedmiot jest ładowany metodą GET (a następnie śledzony przez kontekst) i może nie rozpoznawać tego w twojej metodzie POST jako wcześniej pobranej encji .
Réda Mattar
1
Czy canUserAccessA()ładuje podmiot bezpośrednio czy jako relacja innego podmiotu?
CodeCaster

Odpowiedzi:

155

Problem rozwiązany!

Attachmetoda mogłaby potencjalnie komuś pomóc, ale nie pomogłaby w tej sytuacji, ponieważ dokument był już śledzony podczas ładowania w funkcji kontrolera Edit GET. Attach zwróciłby dokładnie ten sam błąd.

Problem, który tu napotykam, był spowodowany funkcją, canUserAccessA()która ładuje jednostkę A przed aktualizacją stanu obiektu a. To zepsuło śledzoną istotę i zmieniało stan obiektu na Detached.

Rozwiązaniem była zmiana canUserAccessA()tak, aby ładowany przeze mnie obiekt nie był śledzony. Funkcję AsNoTracking()należy wywołać podczas odpytywania kontekstu.

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

Z jakiegoś powodu użycia kulisy .Find(aID)z AsNoTracking()ale to naprawdę nie ma znaczenia, jak mogę osiągnąć to samo poprzez zmianę zapytania.

Mam nadzieję, że pomoże to każdemu z podobnym problemem!

Chris Ciszak
źródło
10
nieco schludniej i wydajniej: if (db.As.AsNoTracking (). Any (x => x.aID == aID && x.UserID == userID))
Brent
11
Uwaga: musisz using System.Data.Entity;użyć AsNoTracking().
Maxime
W moim przypadku aktualizacja tylko pól z wyjątkiem identyfikatora jednostki działała dobrze: var entity = context.Find (entity_id); entity.someProperty = newValue; context.Entry (entity) .Property (x => x.someProperty) .IsModified = true; context.SaveChanges ();
Anton Lyhin
3
Ogromna pomoc. Dodałem .AsNoTracking () przed moim FirstOrDefault () i zadziałało.
coggicc
110

Co ciekawe:

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

Lub jeśli nadal nie jesteś ogólny:

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

wydaje się, że sprawnie rozwiązał mój problem.

guneysus
źródło
1
Niesamowite, to zadziałało idealnie w moim scenariuszu, w którym musiałem zaktualizować rekordy w wielu do wielu za pomocą niestandardowej tabeli łączenia w odłączonej aplikacji. Nawet gdy jednostka została pobrana z bazy danych, otrzymywałem błędy referencyjne itp. Używałem "context.Entry (score) .State = System.Data.Entity.EntityState.Modified;" ale to w końcu zadziałało! Dziękuję Ci!!
firecape
5
To działa. Wszystkie inne sugestie dotyczące dołączania i używania notrackingu zawiodły, ponieważ już robiłem noTracking. Dzięki za rozwiązanie.
Khainestar
3
To zadziałało dla mnie podczas aktualizacji podmiotów nadrzędnych i podrzędnych w tej samej jednostce pracy . wielkie dzięki
Ian
55
Dla każdego, kto szuka, AddOrUpdatejest metodą rozszerzającą w System.Data.Entity.Migrationsprzestrzeni nazw.
Nick
1
@Artyomska Niestety nie wiem.
guneysus
15

Wygląda na to, że obiekt, który próbujesz zmodyfikować, nie jest prawidłowo śledzony i dlatego nie jest rozpoznawany jako edytowany, ale zamiast tego dodawany.

Zamiast bezpośrednio ustawiać stan, spróbuj wykonać następujące czynności:

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

Chciałbym również ostrzec, że Twój kod zawiera potencjalną lukę w zabezpieczeniach. Jeśli używasz encji bezpośrednio w modelu widoku, ryzykujesz, że ktoś może zmodyfikować zawartość encji poprzez dodanie poprawnie nazwanych pól w przesłanym formularzu. Na przykład, jeśli użytkownik dodał pole wejściowe o nazwie „A.FirstName”, a jednostka zawierałaby takie pole, to wartość zostanie przypisana do modelu widoku i zapisana w bazie danych, nawet jeśli użytkownik nie będzie mógł tego zmienić podczas normalnego działania aplikacji .

Aktualizacja:

Aby obejść wspomnianą wcześniej lukę w zabezpieczeniach, nigdy nie należy ujawniać modelu domeny jako modelu widoku, ale zamiast tego użyć oddzielnego modelu widoku. Wtedy twoja akcja otrzyma viewmodel, który możesz mapować z powrotem do modelu domeny za pomocą jakiegoś narzędzia do mapowania, takiego jak AutoMapper. Dzięki temu użytkownik nie będzie mógł modyfikować poufnych danych.

Oto rozszerzone wyjaśnienie:

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

Kaspars Ozols
źródło
3
Cześć Kaspars, dzięki za wkład. Metoda Attach generuje te same błędy, o których wspomniałem w moim pytaniu. Problem w tym, że funkcja canUserAccessA () ładuje jednostkę, a także zauważony powyżej CodeCaster. Ale mówiąc, że jestem bardzo zainteresowany twoją sugestią dotyczącą bezpieczeństwa. Czy możesz zasugerować, co powinienem zrobić, aby zapobiec takiemu zachowaniu?
Chris Ciszak
Zaktualizowałem moją odpowiedź o dodatkowe informacje o tym, jak zapobiegać lukom w zabezpieczeniach.
Kaspars Ozols
13

Spróbuj tego:

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;
Cássio Batista Pereira
źródło
11

dla mnie źródłem problemu była lokalna kopia. to go rozwiązało

var local = context.Set<Contact>().Local.FirstOrDefault(c => c.ContactId == contact.ContactId);
                if (local != null)
                {
                    context.Entry(local).State = EntityState.Detached;
                }
add-Naan
źródło
10

Mój przypadek polegał na tym, że nie miałem bezpośredniego dostępu do kontekstu EF z mojej aplikacji MVC.

Więc jeśli używasz jakiegoś repozytorium do utrwalania jednostek, właściwe może być po prostu odłączenie jawnie załadowanej jednostki, a następnie ustawienie powiązanej jednostki EntityState na Modified.

Przykładowy (abstrakcyjny) kod:

MVC

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

Magazyn

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

void Detach(A a)
{
   context.Entry(a).EntityState = EntityState.Detached;
}
sefira
źródło
To zadziałało dla mnie, chociaż nie zawracałem sobie głowy używaniem repozytorium do odwoływania się do stanów encji kontekstu.
Eckert
3

Pomyślałem, że podzielę się swoim doświadczeniem na ten temat, chociaż czuję się trochę głupio, że nie zdałem sobie z tego sprawy wcześniej.

Używam wzorca repozytorium z instancjami repozytorium wstrzykniętymi do moich kontrolerów. Konkretne repozytoria tworzą wystąpienie mojego ModelContext (DbContext), który trwa przez cały okres istnienia repozytorium, które jest IDisposableusuwane przez kontroler.

Problem polegał na tym, że mam zmodyfikowaną wersję stempla i wiersza na moich obiektach, więc najpierw je pobierałem, aby porównać z nagłówkami przychodzącymi. Oczywiście to ładowało i śledziło jednostkę, która była następnie aktualizowana.

Poprawka polegała po prostu na zmianie repozytorium z tworzenia nowego kontekstu raz w konstruktorze na następujące metody:

    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

Pozwala to metodom repozytorium na ponowne tworzenie nowej instancji kontekstu przy każdym użyciu przez wywołanie GetDbContextlub użycie poprzedniej instancji, jeśli sobie tego życzą, określając true.

Luke Puplett
źródło
2

Dodałem tę odpowiedź tylko dlatego, że problem jest wyjaśniony na podstawie bardziej złożonego wzorca danych i trudno mi go tutaj zrozumieć.

Stworzyłem dość prostą aplikację. Ten błąd wystąpił podczas edycji akcji POST. Akcja przyjęła ViewModel jako parametr wejściowy. Powodem użycia ViewModel było wykonanie pewnych obliczeń przed zapisaniem rekordu.

Gdy akcja przeszła przez walidację, taką jak if(ModelState.IsValid), moim błędem było rzutowanie wartości z ViewModel do zupełnie nowej instancji Entity. Pomyślałem, że będę musiał utworzyć nową instancję do przechowywania zaktualizowanych danych, a następnie zapisałem taką instancję.

Później zdałem sobie sprawę, że musiałem odczytać rekord z bazy danych:

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

i zaktualizował ten obiekt. Teraz wszystko działa.

Celdor
źródło
2

Miałem ten problem z lokalnym var i po prostu odłączam go w ten sposób:

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

Przyczyny problemów związanych z ładowanymi obiektami z tym samym kluczem, więc najpierw odłączymy ten obiekt i wykonamy aktualizację, aby uniknąć konfliktu między dwoma obiektami z tym samym kluczem

lvl4fi4
źródło
@Artjom B Problem Przyczyny załadowanych obiektów z tym samym kluczem, więc najpierw odłączymy ten obiekt i
wykonamy
2

Miałem podobny problem, po sondowaniu przez 2-3 dni znaleziony plik „.AsNoTracking” powinien zostać usunięty, ponieważ EF nie śledzi zmian i zakłada, że ​​nie ma żadnych zmian, dopóki obiekt nie jest dołączony. Również jeśli nie używamy .AsNoTracking, EF automatycznie wie, który obiekt zapisać / zaktualizować, więc nie ma potrzeby używania Attach / Added.

Prem
źródło
2

Użyj AsNoTracking()miejsca, w którym otrzymujesz zapytanie.

  var result = dbcontext.YourModel.AsNoTracking().Where(x => x.aID == aID && x.UserID==userID).Count();
Abdus Salam Azad
źródło
2

Napotkałem ten błąd, gdzie

  • dwie metody, A i B, w jednym kontrolerze używają tego samego wystąpienia ApplicationDbContext i
  • metoda A zwana metodą 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();
    }

Zmieniłem metodę B, aby mieć instrukcję using i polegać tylko na lokalnej db2 . Po:

    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();
    }
colbybhearn
źródło
1

Podobnie jak mówi Luke Puplett, problem może być spowodowany niewłaściwym usuwaniem lub tworzeniem kontekstu.

W moim przypadku miałem klasę, która przyjęła kontekst o nazwie ContextService:

public class ContextService : IDisposable
{
    private Context _context;

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

Moja usługa kontekstowa miała funkcję, która aktualizuje jednostkę przy użyciu instancji obiektu jednostki:

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

Wszystko było w porządku, mój kontroler, na którym zainicjowałem usługę, był problemem. Mój kontroler pierwotnie wyglądał tak:

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

Zmieniłem to na to i błąd zniknął:

    private static NotificationService _service;
    public TemplateController()
    {
        _service = new NotificationService(new NotificationContext());
    }
    public void Dispose()
    {
        _service.Dispose();
    }
Plaża Jared
źródło
1

Problem ten może być również widoczne w ViewModelcelu EntityModelodwzorowania (za pomocą AutoMapper, itd.) I próby obejmują context.Entry().Statei context.SaveChanges()taki, jak pokazano za pomocą bloku poniżej może rozwiązać ten problem. Należy pamiętać, że context.SaveChanges()metoda jest używana dwa razy zamiast używać tuż po, if-blockponieważ musi być również używana przy użyciu bloku.

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

Mam nadzieję że to pomoże...

Murat Yıldız
źródło
1

Tutaj co zrobiłem w podobnej sprawie.

To zadowolenie oznacza, że ​​ta sama istota już istniała w kontekście, więc podążanie może pomóc

Najpierw sprawdź z ChangeTracker, czy jednostka jest w kontekście

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

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

Jeśli istnieje

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

else
{
//Attach or Modify depending on your needs
}
erhan355
źródło
1

Udaje mi się rozwiązać problem, aktualizując stan. kiedy wyzwalasz wyszukiwanie lub jakakolwiek inna operacja kwerendy na tym samym stanie rekordu została zaktualizowana i zmodyfikowana, więc musimy ustawić status na Odłączony, wtedy możesz uruchomić zmianę aktualizacji

     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;
            }
Veera Induvasi
źródło
1

Rozwiązuję ten problem za pomocą bloku „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)
{

    }

Tutaj mam pomysł https://social.msdn.microsoft.com/Forums/sqlserver/es-ES/b4b350ba-b0d5-464d-8656-8c117d55b2af/problema-al-modificar-en-entity-framework?forum = vcses jest po hiszpańsku (poszukaj drugiej odpowiedzi)

Suzume
źródło
po prostu uważać i używać tylko 1 instancję Conexion bazy danych, zwłaszcza jeśli używasz Entity Framework, jeśli nie rób tego dostaniesz błąd Entity Framework Podmiot obiekt nie może być określany przez wielu wystąpień IEntityChangeTracker
Suzume
1

możesz użyć dodanej metody, takiej jak;

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

ale w wielu przypadkach, jeśli chcesz użyć więcej niż jednego modelu w tym czasie, to nie zadziała, ponieważ jednostka jest już dołączona do innej encji. Więc w tym czasie możesz użyć metody ADDOrUpdate Entity Migration, która po prostu migruje obiekt z jednego do drugiego, dzięki czemu nie otrzymasz żadnego błędu.

_dbContext.Set<modelclassname>().AddOrUpdate(yourmodel);
mihir doshi
źródło
0

Wyczyść cały stan

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

xxxsenatorxxx
źródło