ASP.NET MVC - La connexion d'une entité de type 'MODELNAME' a échoué car une autre entité du même type a déjà la même valeur de clé primaire

122

En un mot, l'exception est levée lors du POSTing du modèle de wrapper et en changeant l'état d'une entrée en «Modifié». Avant de changer l'état, l'état est défini sur «Détaché» mais l'appel de Attach () renvoie la même erreur. J'utilise EF6.

Veuillez trouver mon code ci-dessous (les noms des modèles ont été modifiés pour le rendre plus facile à lire)

Modèle

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

Manette

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

Comme indiqué ci-dessus la ligne

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

lève une exception:

L'association d'une entité de type «A» a échoué car une autre entité du même type a déjà la même valeur de clé primaire. Cela peut se produire lorsque vous utilisez la méthode «Attacher» ou que vous définissez l'état d'une entité sur «Inchangé» ou «Modifié» si des entités du graphique ont des valeurs de clé en conflit. Cela peut être dû au fait que certaines entités sont nouvelles et n'ont pas encore reçu de valeurs de clé générées par la base de données. Dans ce cas, utilisez la méthode «Ajouter» ou l'état d'entité «Ajouté» pour suivre le graphique, puis définissez l'état des entités non nouvelles sur «Inchangé» ou «Modifié» selon le cas.

Est-ce que quelqu'un voit quelque chose de mal dans mon code ou comprend dans quelles circonstances cela générerait une telle erreur lors de l'édition d'un modèle?

Chris Ciszak
la source
Avez-vous essayé de joindre votre entité avant de définir le EntityState? Comme votre entité provient d'une demande de publication, elle ne doit pas être suivie par le contexte actuel, je suppose qu'elle considère que vous essayez d'ajouter un élément avec un identifiant existant
Réda Mattar
J'ai essayé celui-ci et le résultat est exactement le même: (Pour une raison quelconque, le contexte pense que je crée un nouvel élément, mais je ne
fais
Je vérifie l'état de «a» avant que l'erreur ne soit générée et l'état de cet objet est «Détaché» mais l'appel de db.As.Attach (aViewModel.a) renvoie exactement le même message? Des idées?
Chris Ciszak
5
Je viens de voir votre mise à jour, comment avez-vous configuré votre portée de vie de contexte? Est-ce par demande? Si l' dbinstance est la même entre vos deux actions, elle peut expliquer votre problème, car votre élément est chargé par la méthode GET (puis suivi par le contexte), et il se peut qu'il ne reconnaisse pas celui de votre méthode POST comme l'entité récupérée auparavant .
Réda Mattar
1
canUserAccessA()Charge- elle l'entité directement ou en tant que relation d'une autre entité?
CodeCaster

Réponses:

155

Problème résolu!

AttachLa méthode pourrait potentiellement aider quelqu'un, mais cela n'aiderait pas dans cette situation car le document était déjà en cours de suivi lors de son chargement dans la fonction de contrôleur Edit GET. Attach générerait exactement la même erreur.

Le problème que je rencontre ici a été causé par la fonction canUserAccessA()qui charge l'entité A avant de mettre à jour l'état de l'objet a. Cela foutait en l'air l'entité suivie et cela changeait l'état d'un objet en Detached.

La solution était de modifier canUserAccessA()pour que l'objet que je chargeais ne soit pas suivi. La fonction AsNoTracking()doit être appelée lors de l'interrogation du contexte.

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

Pour une raison quelconque, je ne pourrais pas utiliser .Find(aID)avec AsNoTracking()mais cela n'a pas vraiment d'importance car je pourrais obtenir la même chose en modifiant la requête.

J'espère que cela aidera quiconque ayant un problème similaire!

Chris Ciszak
la source
10
légèrement plus soigné et plus performant: if (db.As.AsNoTracking (). Any (x => x.aID == aID && x.UserID == userID))
Brent
11
Remarque: vous devez using System.Data.Entity;utiliser AsNoTracking().
Maxime
Dans mon cas, la mise à jour uniquement des champs à l'exception de l'ID d'entité a bien fonctionné: var entity = context.Find (entity_id); entity.someProperty = newValue; context.Entry (entité) .Property (x => x.someProperty) .IsModified = true; context.SaveChanges ();
Anton Lyhin
3
Aide massive. J'ai ajouté le .AsNoTracking () avant mon FirstOrDefault () et cela a fonctionné.
coggicc
110

De façon intéressante:

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

Ou si vous n'êtes toujours pas générique:

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

semble résoudre mon problème en douceur.

guneysus
la source
1
Incroyable, cela fonctionnait parfaitement dans mon scénario où je devais mettre à jour les enregistrements de plusieurs à plusieurs avec une table de jointure personnalisée dans une application déconnectée. Même avec l'entité extraite de la base de données, j'obtenais des erreurs de référence, etc. J'utilisais "context.Entry (score) .State = System.Data.Entity.EntityState.Modified;" mais cela a finalement fonctionné! Je vous remercie!!
Firecape
5
Cela marche. Toutes les autres suggestions sur l'attachement et l'utilisation de notracking ont échoué car je faisais déjà noTracking. Merci pour la solution.
Khainestar
3
Cela a fonctionné pour moi tout en mettant à jour les entités parent et enfant au sein de la même unité de travail . merci beaucoup
Ian
55
Pour ceux qui recherchent, AddOrUpdateest une méthode d'extension dans l' System.Data.Entity.Migrationsespace de noms.
Nick
1
@Artyomska Malheureusement, je ne sais pas.
guneysus
15

Il semble que l'entité que vous essayez de modifier n'est pas correctement suivie et n'est donc pas reconnue comme modifiée, mais ajoutée à la place.

Au lieu de définir directement l'état, essayez de faire ce qui suit:

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

De plus, je voudrais vous avertir que votre code contient une vulnérabilité de sécurité potentielle. Si vous utilisez une entité directement dans votre modèle de vue, vous risquez que quelqu'un modifie le contenu de l'entité en ajoutant des champs correctement nommés dans le formulaire soumis. Par exemple, si l'utilisateur ajoutait une zone de saisie avec le nom "A.FirstName" et que l'entité contenait ce champ, la valeur serait liée à viewmodel et enregistrée dans la base de données même si l'utilisateur ne serait pas autorisé à modifier cela dans le fonctionnement normal de l'application .

Mettre à jour:

Pour surmonter la vulnérabilité de sécurité mentionnée précédemment, vous ne devez jamais exposer votre modèle de domaine en tant que modèle de vue, mais utiliser plutôt un modèle de vue distinct. Ensuite, votre action recevrait viewmodel que vous pourriez mapper au modèle de domaine à l'aide d'un outil de cartographie comme AutoMapper. Cela vous empêcherait de modifier des données sensibles par l'utilisateur.

Voici une explication détaillée:

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

Kaspars Ozols
la source
3
Salut Kaspars, merci pour votre contribution. La méthode Attach génère les mêmes erreurs que celles mentionnées dans ma question. Le problème est que la fonction canUserAccessA () charge l'entité ainsi que CodeCaster remarqué ci-dessus. Mais en disant que je suis très intéressé par votre suggestion en matière de sécurité. Pouvez-vous suggérer ce que je dois faire pour empêcher un tel comportement?
Chris Ciszak
J'ai mis à jour ma réponse avec des informations supplémentaires sur la façon de prévenir les failles de sécurité.
Kaspars Ozols
13

Essaye ça:

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
la source
11

pour moi, la copie locale était la source du problème. cela l'a résolu

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

Mon cas était que je n'avais pas d'accès direct au contexte EF à partir de mon application MVC.

Donc, si vous utilisez une sorte de référentiel pour la persistance des entités, il pourrait être approprié de simplement détacher l'entité explicitement chargée, puis de définir EntityState lié sur Modified.

Exemple de code (abstrait):

MVC

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

Dépôt

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

void Detach(A a)
{
   context.Entry(a).EntityState = EntityState.Detached;
}
séphirot
la source
Cela a fonctionné pour moi, même si je n'ai pas pris la peine d'utiliser un référentiel pour référencer les états d'entité de contexte.
Eckert
3

J'ai pensé partager mon expérience sur celui-ci, même si je me sens un peu idiot de ne pas m'en être rendu compte plus tôt.

J'utilise le modèle de référentiel avec les instances de repo injectées dans mes contrôleurs. Les référentiels concrets instancient mon ModelContext (DbContext) qui dure toute la durée de vie du référentiel, qui est IDisposableet éliminé par le contrôleur.

Le problème pour moi était que j'avais un tampon et une version de ligne modifiés sur mes entités, donc je les recevais en premier afin de les comparer avec les en-têtes entrants. Bien sûr, cela chargeait et suivait l'entité qui était par la suite mise à jour.

Le correctif consistait simplement à changer le référentiel de la création d'un contexte une fois dans le constructeur à l'utilisation des méthodes suivantes:

    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

Cela permet aux méthodes du référentiel de renouveler leur instance de contexte à chaque utilisation en appelant GetDbContext, ou d'utiliser une instance précédente si elles le souhaitent en spécifiant true.

Luke Puplett
la source
2

J'ai ajouté cette réponse uniquement parce que le problème est expliqué sur la base de modèles de données plus complexes et que j'ai eu du mal à comprendre ici.

J'ai créé une application assez simple. Cette erreur s'est produite dans l'action Edit POST. L'action a accepté ViewModel comme paramètre d'entrée. La raison d'utiliser le ViewModel était de faire un calcul avant que l'enregistrement ne soit sauvegardé.

Une fois l'action passée par la validation telle que if(ModelState.IsValid), mon acte répréhensible a été de projeter des valeurs de ViewModel dans une instance complètement nouvelle d'Entity. Je pensais que je devrais créer une nouvelle instance pour stocker les données mises à jour, puis enregistrer cette instance.

Ce que j'avais réalisé plus tard, c'était que je devais lire l'enregistrement à partir de la base de données:

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

et mis à jour cet objet. Tout fonctionne maintenant.

Celdor
la source
2

J'ai eu ce problème avec le var local et je le détache simplement comme ceci:

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

Les causes du problème des objets chargés avec la même clé, nous allons donc d'abord détacher cet objet et faire la mise à jour pour éviter les conflits entre deux objets avec la même clé

lvl4fi4
la source
@Artjom B Les causes du problème des objets chargés avec la même clé, nous allons donc d'abord détacher cet objet et faire la mise à jour pour éviter les conflits entre deux objets avec la même clé
lvl4fi4
2

J'ai eu un problème similaire, après avoir sondé pendant 2-3 jours, ".AsNoTracking" devrait être supprimé car EF ne suit pas les modifications et suppose qu'il n'y a pas de modifications à moins qu'un objet ne soit attaché. De plus, si nous n'utilisons pas .AsNoTracking, EF sait automatiquement quel objet enregistrer / mettre à jour, il n'est donc pas nécessaire d'utiliser Attach / Added.

Prem
la source
2

Utilisez AsNoTracking()là où vous obtenez votre requête.

  var result = dbcontext.YourModel.AsNoTracking().Where(x => x.aID == aID && x.UserID==userID).Count();
Abdus Salam Azad
la source
2

J'ai rencontré cette erreur où

  • deux méthodes, A et B, dans un seul contrôleur ont toutes deux utilisé la même instance d'un ApplicationDbContext, et
  • méthode A appelée méthode 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();
    }

J'ai changé la méthode B pour avoir une instruction using et me fier uniquement au db2 local . Après:

    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
la source
1

Semblable à ce que dit Luke Puplett, le problème peut être causé par une mauvaise suppression ou création de votre contexte.

Dans mon cas, j'avais une classe qui acceptait un contexte appelé ContextService:

public class ContextService : IDisposable
{
    private Context _context;

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

Mon service de contexte avait une fonction qui met à jour une entité à l'aide d'un objet entité instancié:

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

Tout cela allait bien, mon contrôleur où j'ai initialisé le service était le problème. Ma manette ressemblait à l'origine à ceci:

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

Je l'ai changé en ceci et l'erreur a disparu:

    private static NotificationService _service;
    public TemplateController()
    {
        _service = new NotificationService(new NotificationContext());
    }
    public void Dispose()
    {
        _service.Dispose();
    }
Plage de Jared
la source
1

Ce problème peut également être vu pendant ViewModelle EntityModelmappage (en utilisant AutoMapper, etc.) et en essayant d'inclure context.Entry().Stateet context.SaveChanges()un tel bloc d'utilisation comme indiqué ci-dessous résoudrait le problème. Veuillez garder à l'esprit que la context.SaveChanges()méthode est utilisée deux fois au lieu d'utiliser juste après, if-blockcar elle doit l'être également avec 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
        }
    }            
}

J'espère que cela t'aides...

Murat Yıldız
la source
1

Voici ce que j'ai fait dans le cas similaire.

Cette situation signifie que la même entité a déjà existé dans le contexte.

Vérifiez d'abord depuis ChangeTracker si l'entité est dans le contexte

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

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

S'il existe

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

else
{
//Attach or Modify depending on your needs
}
erhan355
la source
1

Je parviens à résoudre le problème en mettant à jour l'état. lorsque vous déclenchez la recherche ou toute autre opération de requête sur le même état d'enregistrement a été mis à jour avec modifié, nous devons donc définir le statut sur Détaché, vous pouvez déclencher votre modification de mise à jour

     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
la source
1

Je résous ce problème avec un bloc "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)
{

    }

Voici où j'ai l'idée https://social.msdn.microsoft.com/Forums/sqlserver/es-ES/b4b350ba-b0d5-464d-8656-8c117d55b2af/problema-al-modificar-en-entity-framework?forum = vcses est en espagnol (cherchez la deuxième réponse)

Suzume
la source
soyez juste prudent et n'utilisez qu'une seule instance de conexion de base de données, spécialement si vous utilisez le framework d'entité si vous ne le faites pas, vous obtiendrez l'erreur Entity Framework Un objet entité ne peut pas être référencé par plusieurs instances de IEntityChangeTracker
Suzume
1

vous pouvez utiliser une méthode supplémentaire comme;

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

mais dans de nombreux cas, si vous souhaitez utiliser plus d'un modèle à ce moment-là, cela ne fonctionnera pas car l'entité est déjà attachée à une autre entité. Ainsi, à ce moment-là, vous pouvez utiliser la méthode ADDOrUpdate Entity Migration qui migre simplement l'objet de l'un à l'autre et, par conséquent, vous n'obtiendrez aucune erreur.

_dbContext.Set<modelclassname>().AddOrUpdate(yourmodel);
mihir doshi
la source
0

Effacer tout l'état

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

xxxsenatorxxx
la source