69

Usando EF5 con un patrón de repositorio genérico y ninject para la detección de dependencias y un problema cuando intento actualizar una entidad a la base de datos utilizando procesos almacenados con mi edmx.Ya existe un objeto con la misma clave en ObjectStateManager. El ObjectStateManager no puede rastrear múltiples objetos con la misma clave

mi actualización en DbContextRepository.cs es:

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 mis AddressService.cs que se remonta a mi repositorio que tienen:

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

    _repo.SaveChanges(); 

    return address.address_pk; 
} 

cuando golpea el Adjuntar y se EntityState.Modified vomita con el error:

Ya existe un objeto con la misma clave en ObjectStateManager. ObjectStateManager no puede rastrear múltiples objetos con la misma clave.

He revisado muchas de las sugerencias en pila y en Internet y no he encontrado nada que lo resuelva. Cualquier solución sería apreciada.

Gracias!

Respuesta

125

Editar: Respuesta original utilizada Find en lugar de Local.SingleOrDefault. Funcionó en combinación con el método Save de @ Juan pero podría causar consultas innecesarias a la base de datos y else probablemente nunca se ejecutó la parte (ejecutar la parte else causaría una excepción porque Find ya había consultado la base de datos y no había encontrado la entidad por lo que no podría actualizado). Gracias a @BenSwayne por encontrar el problema.

Debe comprobar si una entidad con la misma clave que ya es rastreado por el contexto y modificar esa entidad en lugar de asociar el actual:

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

Como se puede ver el problema principal es que SingleOrDefault necesidades método para saber la clave para encontrar la entidad. Puede crear una interfaz simple exponiendo la clave (IEntity en mi ejemplo) e implementarla en todas las entidades que desee procesar de esta manera.

+0

Gracias. Así que creé una interfaz IEntity con int Id {get; conjunto; } luego intenté hacer público anular la actualización vacía (entidad T) donde T: IEntity pero no le gusta el lugar donde T: IEntity. Esto se encuentra en una clase de repositorio, es decir, clase pública DbContextRepository : BaseRepository donde T: clase si eso hace la diferencia. ¡Gracias! – Juan

+1

En tal caso, coloque la restricción directamente en la definición de clase –

+0

hmm .. todavía no tiene mucha suerte. Me pregunto si es porque estoy usando un modelo de Edmx. Pero no puedo poner la restricción directamente en la clase ya que implementa BaseRepository e IRepository. Además en el edmx, las entidades provienen de vistas y las claves principales son algo así como address_pk. – Juan

-2

Esa respuesta anterior puede ser EF 4.1+. Para aquellos en 4.0, pruebe este método simple ... realmente no probado, pero sí adjunté y guardo mis cambios.

public void UpdateRiskInsight(RiskInsight item) 
    { 
     if (item == null) 
     { 
      throw new ArgumentException("Cannot add a null entity."); 
     } 

     if (item.RiskInsightID == Guid.Empty) 
     { 
      _db.RiskInsights.AddObject(item); 
     } 
     else 
     { 
      item.EntityKey = new System.Data.EntityKey("GRC9Entities.RiskInsights", "RiskInsightID", item.RiskInsightID); 
      var entry = _db.GetObjectByKey(item.EntityKey) as RiskInsight; 
      if (entry != null) 
      { 
       _db.ApplyCurrentValues<RiskInsight>("GRC9Entities.RiskInsights", item); 
      } 

     } 

     _db.SaveChanges(); 

    } 
+1

Parece que omite por completo el hecho de que el OP desea hacer esto con un repositorio genérico. –

7

En realidad puede retreive la identificación mediante la reflexión, véase el siguiente ejemplo:

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

No estoy seguro de cómo obtendría '_dbset' de forma genérica ... –

+0

@SerjSagan Simplemente puede hacer' _dbContext.Set () .Crear(). GetTy..' –

2

@serj-sagan debe hacerlo de esta manera:

** Tenga en cuenta que basedatos debe ser una clase derivado 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 
      } 
     } 
    } 

}

0

Sin reflexión y si no desea utilizar las interfaces, puede utilizar delegados funcionales para encontrar una entidad en la base de datos. Aquí está la muestra actualizada desde arriba.

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

Se podría llamar así:

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

Yo no quiero contaminar mi auto generada clases de EF por las interfaces, añadiendo o atributos. así que esto es realmente un poco de algunas de las respuestas anteriores (así que el crédito va para Ladislav Mrnka). Esto proporcionó una solución simple para mí.

Agregué un func al método de actualización que encontró la clave entera de la entidad.

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

Luego, cuando llame a su código, puede utilizar ..

repository.Update(entity, key => key.myId); 
+0

En caso de que no esté usando 'set. Local.Find' en lugar de 'set.Find'? Creo que su código siempre golpeará la base de datos, y nunca hará que la variable 'attachedEntity' sea nula. https://msdn.microsoft.com/en-us/library/jj592872(v=vs.113).aspx –

1

Otra solución (basado en la @ respuesta de Sergey) podría ser:

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

Y luego lo llamaría así:

Update(EntitytoUpdate, key => key.Id == id) 
-3

Es posible que haya olvidado instalar el objeto fBLL = new FornecedorBLL(); i n algun place

0

Separar la entidad encontrada (ver attachedEntity en Ladislav's solution) y volver a adjuntar la modificada funcionó bien para mí.

El razonamiento detrás de esto es simple: si algo es inmutable, reemplácelo (como un todo, entidad) desde donde pertenece al deseado.

Aquí está un ejemplo de cómo hacer esto:

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

Por supuesto, uno puede fácilmente averiguar que este fragmento es parte de un método genérico, de ahí el uso de T, que es un parámetro de plantilla, y Set<T>().

Cuestiones relacionadas