ASP.NET MVC: no se pudo adjuntar una entidad de tipo 'MODELNAME' porque otra entidad del mismo tipo ya tiene el mismo valor de clave principal

122

En pocas palabras, la excepción se lanza durante la publicación del modelo de envoltura y el cambio del estado de una entrada a 'Modificado'. Antes de cambiar el estado, el estado se establece en 'Separado' pero llamar a Attach () arroja el mismo error. Estoy usando EF6.

Encuentre mi código a continuación (los nombres de los modelos se han cambiado para que sea más fácil de leer)

Modelo

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

Controlador

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

Como se muestra arriba de la línea

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

lanza una excepción:

No se pudo adjuntar una entidad de tipo 'A' porque otra entidad del mismo tipo ya tiene el mismo valor de clave principal. Esto puede suceder cuando se usa el método 'Adjuntar' o se establece el estado de una entidad en 'Sin cambios' o 'Modificado' si alguna de las entidades del gráfico tiene valores clave en conflicto. Esto puede deberse a que algunas entidades son nuevas y aún no han recibido valores clave generados por la base de datos. En este caso, use el método 'Agregar' o el estado de la entidad 'Agregado' para rastrear el gráfico y luego establezca el estado de las entidades no nuevas en 'Sin cambios' o 'Modificado' según corresponda.

¿Alguien ve algo incorrecto en mi código o entiende en qué circunstancias arrojaría tal error durante la edición de un modelo?

Chris Ciszak
fuente
¿Ha intentado adjuntar su entidad antes de configurar el EntityState? Como su entidad proviene de una solicitud de publicación, no debe ser rastreada por el contexto actual, supongo que considera que intenta agregar un elemento con una ID existente
Réda Mattar
Probé este y el resultado es exactamente el mismo :( Por alguna razón, el contexto cree que estoy creando un nuevo elemento, pero solo estoy actualizando el existente ...
Chris Ciszak
Verifico el estado de 'a' antes de que se produzca el error y el estado de este objeto es 'Separado' pero llamar a db.As.Attach (aViewModel.a) arroja exactamente el mismo mensaje. ¿Algunas ideas?
Chris Ciszak
5
Acabo de ver su actualización, ¿cómo configuró el alcance de su vida útil de contexto? ¿Es por solicitud? Si la dbinstancia es la misma entre sus dos acciones, puede explicar su problema, ya que su elemento es cargado por el método GET (luego es rastreado por el contexto), y es posible que no reconozca el de su método POST como la entidad recuperada antes. .
Réda Mattar
1
¿ canUserAccessA()Carga la entidad directamente o como relación de otra entidad?
CodeCaster

Respuestas:

155

¡Problema resuelto!

AttachEl método podría ayudar a alguien, pero no ayudaría en esta situación, ya que el documento ya se estaba rastreando mientras se cargaba en la función Editar controlador GET. Adjuntar arrojaría exactamente el mismo error.

El problema que encuentro aquí fue causado por una función canUserAccessA()que carga la entidad A antes de actualizar el estado del objeto a. Esto estaba arruinando la entidad rastreada y estaba cambiando el estado de un objeto a Detached.

La solución fue modificar canUserAccessA()para que el objeto que estaba cargando no fuera rastreado. Se AsNoTracking()debe llamar a la función mientras se consulta el contexto.

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

Por alguna razón que no podía usar .Find(aID)con AsNoTracking()pero en realidad no importa ya que podía lograr el mismo cambiando la consulta.

¡Espero que esto ayude a cualquiera con un problema similar!

Chris Ciszak
fuente
10
un poco más ordenado y más eficaz: if (db.As.AsNoTracking (). Any (x => x.aID == aID && x.UserID == userID))
Brent
11
Nota: necesitas using System.Data.Entity;usar AsNoTracking().
Maxime
En mi caso, actualizar solo los campos, excepto la identificación de la entidad, funcionó bien: var entity = context.Find (entity_id); entity.someProperty = newValue; context.Entry (entidad) .Property (x => x.someProperty) .IsModified = true; context.SaveChanges ();
Anton Lyhin
3
Ayuda masiva. Agregué .AsNoTracking () antes de mi FirstOrDefault () y funcionó.
coggicc
110

Curiosamente:

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

O si aún no es genérico:

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

parece haber resuelto mi problema sin problemas.

guneysus
fuente
1
Increíble, eso funcionó perfecto en mi escenario donde necesitaba actualizar registros de muchos a muchos con una tabla de combinación personalizada en una aplicación desconectada. Incluso con la entidad extraída de la base de datos, obtenía errores de referencia, etc. Estaba usando "context.Entry (score) .State = System.Data.Entity.EntityState.Modified;" ¡pero esto finalmente funcionó! ¡¡Gracias!!
firecape
5
Esto funciona. Todas las otras sugerencias sobre cómo adjuntar y usar notracking fallaron porque ya estaba haciendo noTracking. Gracias por la solucion
Khainestar
3
Esto funcionó para mí al actualizar las entidades principales y secundarias dentro de la misma unidad de trabajo . muchas gracias
Ian
55
Para cualquiera que busque, AddOrUpdatees un método de extensión en el System.Data.Entity.Migrationsespacio de nombres.
Nick
1
@Artyomska Lamentablemente no lo sé.
guneysus
15

Parece que la entidad que está intentando modificar no se está rastreando correctamente y, por lo tanto, no se reconoce como editada, sino que se agrega.

En lugar de establecer el estado directamente, intente hacer lo siguiente:

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

Además, me gustaría advertirle que su código contiene una vulnerabilidad de seguridad potencial. Si está utilizando la entidad directamente en su modelo de vista, corre el riesgo de que alguien pueda modificar el contenido de la entidad agregando campos con el nombre correcto en el formulario enviado. Por ejemplo, si el usuario agregó un cuadro de entrada con el nombre "A.FirstName" y la entidad contenía dicho campo, entonces el valor se vincularía al modelo de vista y se guardaría en la base de datos, incluso si el usuario no podría cambiarlo en el funcionamiento normal de la aplicación. .

Actualizar:

Para superar la vulnerabilidad de seguridad mencionada anteriormente, nunca debe exponer su modelo de dominio como su modelo de vista, sino utilizar un modelo de vista separado. Entonces su acción recibiría viewmodel que podría mapear de nuevo al modelo de dominio usando alguna herramienta de mapeo como AutoMapper. Esto lo mantendrá a salvo de que el usuario modifique datos confidenciales.

Aquí hay una explicación ampliada:

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

Kaspars Ozols
fuente
3
Hola Kaspars, gracias por tu entrada. El método Attach arroja los mismos errores que se mencionan en mi pregunta. El problema es que la función canUserAccessA () carga la entidad así como CodeCaster mencionado anteriormente. Pero diciendo que estoy muy interesado en su sugerencia con respecto a la seguridad. ¿Podría sugerirme qué debo hacer para prevenir tal comportamiento?
Chris Ciszak
Actualicé mi respuesta con información adicional sobre cómo prevenir la vulnerabilidad de seguridad.
Kaspars Ozols
13

Prueba esto:

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
fuente
11

para mí, la copia local fue la fuente del problema. esto lo resolvió

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

Mi caso fue que no tenía acceso directo al contexto EF desde mi aplicación MVC.

Entonces, si está utilizando algún tipo de repositorio para la persistencia de la entidad, podría ser apropiado simplemente separar la entidad cargada explícitamente y luego establecer EntityState enlazado en Modificado.

Código de muestra (resumen):

MVC

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

Repositorio

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

void Detach(A a)
{
   context.Entry(a).EntityState = EntityState.Detached;
}
sefirot
fuente
Esto funcionó para mí, aunque no me molesté en usar un repositorio para hacer referencia a los estados de la entidad de contexto.
Eckert
3

Pensé en compartir mi experiencia en este caso, aunque me siento un poco tonto por no darme cuenta antes.

Estoy usando el patrón de repositorio con las instancias de repositorio inyectadas en mis controladores. Los repositorios concretos crean una instancia de mi ModelContext (DbContext) que dura la vida útil del repositorio, que es IDisposableeliminado por el controlador.

El problema para mí fue que tengo un sello modificado y una versión de fila en mis entidades, por lo que los estaba obteniendo primero para compararlos con los encabezados entrantes. Por supuesto, esto cargó y rastreó la entidad que posteriormente se estaba actualizando.

La solución fue simplemente cambiar el repositorio de un contexto nuevo una vez en el constructor a tener los siguientes métodos:

    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

Esto permite que los métodos del repositorio renueven su instancia de contexto en cada uso llamando GetDbContext, o usen una instancia anterior si así lo desean especificando true.

Luke Puplett
fuente
2

He agregado esta respuesta solo porque el problema se explica en función de un patrón de datos más complejo y me resultó difícil de entender aquí.

Creé una aplicación bastante simple. Este error ocurrió dentro de la acción Editar POST. La acción aceptó ViewModel como parámetro de entrada. La razón para usar ViewModel fue hacer algunos cálculos antes de guardar el registro.

Una vez que la acción pasó por la validación if(ModelState.IsValid), mi mala acción fue proyectar valores de ViewModel en una instancia completamente nueva de Entity. Pensé que tendría que crear una nueva instancia para almacenar datos actualizados y luego guardé dicha instancia.

Más tarde me di cuenta de que tenía que leer el registro de la base de datos:

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

y actualizó este objeto. Todo funciona ahora.

Celdor
fuente
2

Tuve este problema con la var local y simplemente lo separé así:

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

Causas del problema de objetos cargados con la misma clave, por lo que primero separaremos ese objeto y realizaremos la actualización para evitar conflictos entre dos objetos con la misma clave

lvl4fi4
fuente
@Artjom B Causas del problema de objetos cargados con la misma clave, así que primero separaremos ese objeto y realizaremos la actualización para evitar conflictos entre dos objetos con la misma clave
lvl4fi4
2

Tuve un problema similar, después de sondear durante 2-3 días, se encontró que ".AsNoTracking" debería eliminarse ya que EF no rastrea los cambios y asume que no hay cambios a menos que se adjunte un objeto. Además, si no usamos .AsNoTracking, EF sabe automáticamente qué objeto guardar / actualizar, por lo que no es necesario usar Attach / Added.

Prem
fuente
2

Use AsNoTracking()donde obtiene su consulta.

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

Encontré este error donde

  • dos métodos, A y B, en un solo controlador usaron la misma instancia de un ApplicationDbContext, y
  • método A llamado método 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();
    }

Cambié el método B para tener una declaración de uso y confiar solo en el db2 local . Despué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
fuente
1

De forma similar a lo que dice Luke Puplett, el problema puede deberse a no disponer o crear correctamente su contexto.

En mi caso, tenía una clase que aceptaba un contexto llamado ContextService:

public class ContextService : IDisposable
{
    private Context _context;

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

Mi servicio de contexto tenía una función que actualiza una entidad usando un objeto de entidad instanciado:

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

Todo esto estuvo bien, mi controlador donde inicialicé el servicio fue el problema. Mi controlador originalmente se veía así:

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

Lo cambié a esto y el error desapareció:

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

Este problema también puede ser visto durante ViewModelal EntityModelmapeo (mediante el uso AutoMapper, etc.) y tratando de incluir context.Entry().Statey context.SaveChanges()tal usando bloques como se muestra a continuación resolvería el problema. Tenga en cuenta que el context.SaveChanges()método se usa dos veces en lugar de usar solo después, if-blockya que también debe usarse en el bloque de uso.

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

Espero que esto ayude...

Murat Yıldız
fuente
1

Aquí lo que hice en el caso similar.

Esa situación significa que la misma entidad ya existía en el contexto, por lo que seguir puede ayudar

Primero verifique desde ChangeTracker si la entidad está en el contexto

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

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

Si existiera

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

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

Me las arreglo para solucionar el problema actualizando el estado. cuando activa la búsqueda o cualquier otra operación de consulta en el mismo estado de registro se ha actualizado con modificado, por lo que debemos establecer el estado en Separado, entonces puede activar su cambio de actualización

     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
fuente
1

Resuelvo este problema con un bloque "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)
{

    }

Aquí es donde me sale la idea https://social.msdn.microsoft.com/Forums/sqlserver/es-ES/b4b350ba-b0d5-464d-8656-8c117d55b2af/problema-al-modificar-en-entity-framework?forum = vcses está en español (busque la segunda respuesta)

Suzume
fuente
solo tenga cuidado y solo use 1 instancia de conexión de base de datos, especialmente si está usando el marco de la entidad, si no lo hace, obtendrá el error Entity Framework Un objeto de entidad no puede ser referenciado por múltiples instancias de IEntityChangeTracker
Suzume
1

puede usar un método agregado como;

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

pero en muchos casos, si desea utilizar más de un modelo en ese momento, esto no funcionará porque la entidad ya está adjunta a otra entidad. Entonces, en ese momento puede usar el método ADDOrUpdate Entity Migration que simplemente migra el objeto de uno a otro y, como resultado, no obtendrá ningún error.

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

Borrar todo el estado

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

xxxsenatorxxx
fuente