web-dev-qa-db-fra.com

Un objet avec la même clé existe déjà dans ObjectStateManager. ObjectStateManager ne peut pas suivre plusieurs objets avec la même clé

Utilisation de EF5 avec un modèle de référentiel générique et ninject pour une dépendance et un problème lorsque vous tentez de mettre à jour une entité dans la base de données à l'aide de procédures stockées avec mon edmx.

ma mise à jour dans DbContextRepository.cs est:

public override void Update(T entity)
{
    if (entity == null)
        throw new ArgumentException("Cannot add a null entity.");

    var entry = _context.Entry<T>(entity);

    if (entry.State == EntityState.Detached)
    {
        _context.Set<T>().Attach(entity);
        entry.State = EntityState.Modified;
    }
}

De mon AddressService.cs qui retourne à mon référentiel, j'ai:

 public int Save(vw_address address)
{
    if (address.address_pk == 0)
    {
        _repo.Insert(address);
    }
    else
    {
        _repo.Update(address);
    }

    _repo.SaveChanges();

    return address.address_pk;
}

Quand il frappe l'attachement et EntityState.Modified il vomit avec l'erreur:

n objet avec la même clé existe déjà dans ObjectStateManager. ObjectStateManager ne peut pas suivre plusieurs objets avec la même clé.

J'ai parcouru de nombreuses suggestions dans la pile et sur Internet, sans trouver quoi que ce soit qui résout le problème. Tout travail autour serait apprécié.

Merci!

71
Juan

Edit: La réponse originale utilisait Find au lieu de Local.SingleOrDefault. Cela fonctionnait en combinaison avec la méthode Save de @ Juan, mais cela pourrait causer des requêtes inutiles à la base de données et la partie else n'aurait probablement jamais été exécutée (l'exécution de la partie else causerait une exception car Find avait déjà interrogé la base de n’a pas trouvé l’entité afin qu’elle ne puisse pas être mise à jour). Merci à @BenSwayne d'avoir trouvé le problème.

Vous devez vérifier si une entité avec la même clé est déjà suivie par le contexte et modifier cette entité au lieu de joindre celle en cours:

public override void Update(T entity) where T : IEntity {
    if (entity == null) {
        throw new ArgumentException("Cannot add a null entity.");
    }

    var entry = _context.Entry<T>(entity);

    if (entry.State == EntityState.Detached) {
        var set = _context.Set<T>();
        T attachedEntity = set.Local.SingleOrDefault(e => e.Id == entity.Id);  // You need to have access to key

        if (attachedEntity != null) {
            var attachedEntry = _context.Entry(attachedEntity);
            attachedEntry.CurrentValues.SetValues(entity);
        } else {
            entry.State = EntityState.Modified; // This should attach entity
        }
    }
}  

Comme vous pouvez le constater, le problème principal est que la méthode SingleOrDefault a besoin de connaître la clé pour trouver l'entité. Vous pouvez créer une interface simple exposant la clé (IEntity dans mon exemple) et l'implémenter dans toutes vos entités que vous souhaitez traiter de cette manière.

125
Ladislav Mrnka

Je ne voulais pas polluer mes classes EF générées automatiquement en ajoutant des interfaces ou des attributs. c'est donc un peu certaines des réponses ci-dessus (le mérite revient donc à Ladislav Mrnka). Cela a fourni une solution simple pour moi.

J'ai ajouté un func à la méthode de mise à jour qui a trouvé la clé entière de l'entité.

public void Update(TEntity entity, Func<TEntity, int> getKey)
{
    if (entity == null) {
        throw new ArgumentException("Cannot add a null entity.");
    }

    var entry = _context.Entry<T>(entity);

    if (entry.State == EntityState.Detached) {
        var set = _context.Set<T>();
        T attachedEntity = set.Find.(getKey(entity)); 

        if (attachedEntity != null) {
            var attachedEntry = _context.Entry(attachedEntity);
            attachedEntry.CurrentValues.SetValues(entity);
        } else {
            entry.State = EntityState.Modified; // This should attach entity
        }
    }
}  

Ensuite, lorsque vous appelez votre code, vous pouvez utiliser ..

repository.Update(entity, key => key.myId);
8
Geoff Wells

Vous pouvez réellement récupérer l'identifiant par réflexion, voir l'exemple ci-dessous:

        var entry = _dbContext.Entry<T>(entity);

        // Retreive the Id through reflection
        var pkey = _dbset.Create().GetType().GetProperty("Id").GetValue(entity);

        if (entry.State == EntityState.Detached)
        {
            var set = _dbContext.Set<T>();
            T attachedEntity = set.Find(pkey);  // access the key
            if (attachedEntity != null)
            {
                var attachedEntry = _dbContext.Entry(attachedEntity);
                attachedEntry.CurrentValues.SetValues(entity);
            }
            else
            {
                entry.State = EntityState.Modified; // attach the entity
            }
        }
7
Toffee

@ serj-sagan vous devriez le faire de cette façon:

** Notez que YourDb doit être une classe dérivée de DbContext.

public abstract class YourRepoBase<T> where T : class
{
    private YourDb _dbContext;
    private readonly DbSet<T> _dbset;

    public virtual void Update(T entity)
    {
        var entry = _dbContext.Entry<T>(entity);

        // Retreive the Id through reflection
        var pkey = _dbset.Create().GetType().GetProperty("Id").GetValue(entity);

        if (entry.State == EntityState.Detached)
        {
           var set = _dbContext.Set<T>();
           T attachedEntity = set.Find(pkey);  // access the key
           if (attachedEntity != null)
           {
               var attachedEntry = _dbContext.Entry(attachedEntity);
               attachedEntry.CurrentValues.SetValues(entity);
           }
           else
           {
              entry.State = EntityState.Modified; // attach the entity
           }
       }
    }

}

2
Toffee

Une autre solution (basée sur la réponse de @ Sergey) pourrait être:

private void Update<T>(T entity, Func<T, bool> predicate) where T : class
{
    var entry = Context.Entry(entity);
    if (entry.State == EntityState.Detached)
    {
        var set = Context.Set<T>();
        T attachedEntity = set.Local.SingleOrDefault(predicate); 
        if (attachedEntity != null)
        {
            var attachedEntry = Context.Entry(attachedEntity);
            attachedEntry.CurrentValues.SetValues(entity);
        }
        else
        {
            entry.State = EntityState.Modified; // This should attach entity
        }
    }
}

Et puis, tu l'appellerais comme ça:

Update(EntitytoUpdate, key => key.Id == id)
1
Andrés S.

Si vous définissez votre contexte sur AsNoTracking (), cela arrêtera aspmvc de suivre les modifications apportées à l'entité en mémoire (ce que vous voulez de toute façon sur le Web).

_dbContext.Products.AsNoTracking().Find(id);  

Je vous recommanderais d'en lire plus à ce sujet sur http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/advanced-entity-framework- scénarios-pour-une-application-web-mvc

0
Yashvit

Sans réflexion et si vous ne souhaitez pas utiliser d'interfaces, vous pouvez utiliser des délégués fonctionnels pour rechercher une entité dans la base de données. Voici l'exemple mis à jour ci-dessus.

private void Update<T>(T entity, Func<ObservableCollection<T>, T> locatorMap) where T : class
{
    var entry = Context.Entry(entity);
    if (entry.State == EntityState.Detached)
    {
        var set = Context.Set<T>();
        T attachedEntity = locatorMap(set.Local); 

        if (attachedEntity != null)
        {
            var attachedEntry = Context.Entry(attachedEntity);
            attachedEntry.CurrentValues.SetValues(entity);
        }
        else
        {
            entry.State = EntityState.Modified; // This should attach entity
        }
    }
}

Vous l'appelleriez comme ceci:

Update(EntitytoUpdate, p => p.SingleOrDefault(a => a.Id == id))
0
Sergey

Détacher l'entité trouvée (voir la solution attachedEntity dans de Ladislav ) et attacher de nouveau la cellule modifiée a parfaitement fonctionné pour moi.

Le raisonnement derrière cela est simple: si quelque chose est immuable, remplacez-le (dans son ensemble, par une entité) d'où il appartient par celui désiré.

Voici un exemple de comment faire cela:

var set = this.Set<T>();
if (this.Entry(entity).State == EntityState.Detached)
{
    var attached = set.Find(id);
    if (attached != null) { this.Entry(attached).State = EntityState.Detached; }
    this.Attach(entity);
}

set.Update(entity);

Bien sûr, on peut facilement comprendre que cet extrait fait partie d’une méthode générique, d’où l’utilisation de T, qui est un paramètre de modèle, et de Set<T>().

0
Alexander Christov