Anexando uma entidade do tipo 'MODELNAME' falhou porque outra entidade do mesmo tipo já possui o mesmo valor de chave primária

121

Em poucas palavras, a exceção é lançada durante o modelo de wrapper POSTing e alterando o estado de uma entrada para 'Modificado'. Antes de alterar o estado, o estado é definido como 'Desconectado', mas chamar Attach () gera o mesmo erro. Eu estou usando EF6.

Encontre meu código abaixo (os nomes dos modelos foram alterados para facilitar a leitura)

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 mostrado acima da linha

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

lança exceção:

Falha ao anexar uma entidade do tipo 'A' porque outra entidade do mesmo tipo já possui o mesmo valor de chave primária. Isso pode acontecer ao usar o método 'Anexar' ou ao definir o estado de uma entidade como 'Inalterado' ou 'Modificado' se alguma entidade no gráfico tiver valores-chave conflitantes. Isso pode ocorrer porque algumas entidades são novas e ainda não receberam valores-chave gerados pelo banco de dados. Nesse caso, use o método 'Adicionar' ou o estado da entidade 'Adicionado' para rastrear o gráfico e defina o estado de entidades não novas como 'Inalterado' ou 'Modificado', conforme apropriado.

Alguém vê algo errado no meu código ou entende em que circunstâncias isso geraria esse erro durante a edição de um modelo?

Chris Ciszak
fonte
Você já tentou anexar sua entidade antes de definir o EntityState? Como sua entidade vem de uma solicitação post, ele não deve ser monitorado pelo contexto atual, eu acho que considera que tentar adicionar um item com um ID existente
Réda Mattar
Eu tentei este e o resultado é exatamente o mesmo :( Para alguns contexto razão pensa Im criar um novo item, mas Im apenas atualizar o existente ...
Chris Ciszak
Eu verifico o estado de 'a' antes que o erro seja gerado e o estado desse objeto seja 'Detached', mas chamar db.As.Attach (aViewModel.a) lança exatamente a mesma mensagem? Alguma ideia?
precisa saber é o seguinte
5
Acabei de ver sua atualização, como você configurou seu escopo de duração do contexto? É por solicitação? Se a dbinstância for a mesma entre suas duas ações, ela poderá explicar seu problema, pois o item será carregado pelo método GET (depois rastreado pelo contexto) e poderá não reconhecer o item no método POST como a entidade buscada antes .
Réda Mattar 21/04
1
Será que canUserAccessA()carregar a entidade direta ou como uma relação de outra entitiy?
CodeCaster

Respostas:

154

Problema resolvido!

AttachO método poderia potencialmente ajudar alguém, mas não ajudaria nessa situação, pois o documento já estava sendo rastreado durante o carregamento na função do controlador Edit GET. Anexar lançaria exatamente o mesmo erro.

O problema que encontrei aqui foi causado pela função canUserAccessA()que carrega a entidade A antes de atualizar o estado do objeto a. Isso estava estragando a entidade rastreada e estava mudando o estado de um objeto para Detached.

A solução foi alterar canUserAccessA()para que o objeto que eu estava carregando não fosse rastreado. A função AsNoTracking()deve ser chamada durante a consulta do 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 alguma razão, eu não poderia usá .Find(aID)- AsNoTracking()lo, mas isso realmente não importa, pois eu poderia conseguir o mesmo alterando a consulta.

Espero que isso ajude alguém com problema semelhante!

Chris Ciszak
fonte
10
Um pouco mais limpo e com melhor desempenho: if (db.As.AsNoTracking (). Any (x => x.aID == aID && x.UserID == userID))
Brent
11
Nota: você precisa using System.Data.Entity;usar AsNoTracking().
Maxime
No meu caso, atualizar apenas os campos, exceto o ID da entidade, funcionou bem: var entity = context.Find (entity_id); entity.someProperty = newValue; context.Entry (entity) .Property (x => x.someProperty) .IsModified = true; context.SaveChanges ();
Anton Lyhin
3
Ajuda maciça. Eu adicionei o .AsNoTracking () antes do meu FirstOrDefault () e funcionou.
Coggicc 01/03
109

Curiosamente:

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

Ou se você ainda não é genérico:

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

parece ter resolvido meu problema sem problemas.

guneysus
fonte
1
Incrível, isso funcionou perfeitamente no meu cenário em que eu precisava atualizar registros de muitos para muitos com uma tabela de junção personalizada em um aplicativo desconectado. Mesmo com a entidade retirada do banco de dados, eu estava recebendo erros referenciais, etc. Eu estava usando "context.Entry (score) .State = System.Data.Entity.EntityState.Modified;" mas isso finalmente funcionou! Obrigado!!
firecape 21/01
5
Isso funciona. Todas as outras sugestões sobre como anexar e usar o notracking falharam, pois eu já estava fazendo o noTracking. Obrigado pela solução.
Khainestar
3
Isso funcionou para mim enquanto atualizava entidades pai e filho na mesma unidade de trabalho . muito obrigado
Ian
55
Para quem procura, AddOrUpdateé um método de extensão no System.Data.Entity.Migrationsespaço para nome.
31416 Nick
1
@Artyomska Infelizmente eu não sei.
27917 GunHeysus
15

Parece que a entidade que você está tentando modificar não está sendo rastreada corretamente e, portanto, não é reconhecida como editada, mas adicionada.

Em vez de definir diretamente o estado, tente fazer o seguinte:

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

Além disso, gostaria de avisar que seu código contém uma potencial vulnerabilidade de segurança. Se você estiver usando uma entidade diretamente no seu modelo de exibição, corre o risco de alguém modificar o conteúdo da entidade adicionando campos nomeados corretamente no formulário enviado. Por exemplo, se o usuário incluísse a caixa de entrada com o nome "A.FirstName" e a entidade contivesse esse campo, o valor seria vinculado ao viewmodel e salvo no banco de dados, mesmo que o usuário não tivesse permissão para alterá-lo na operação normal do aplicativo .

Atualizar:

Para superar a vulnerabilidade de segurança mencionada anteriormente, você nunca deve expor seu modelo de domínio como seu modelo de exibição, mas use um modelo de exibição separado. Em seguida, sua ação receberá um modelo de visualização que você poderá mapear de volta para o modelo de domínio usando alguma ferramenta de mapeamento como o AutoMapper. Isso impediria a modificação de dados confidenciais pelo usuário.

Aqui está uma explicação estendida:

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

Kaspars Ozols
fonte
3
Olá Kaspars, obrigado pela contribuição. O método Attach lança os mesmos erros mencionados na minha pergunta. O problema é que a função canUserAccessA () carrega a entidade, bem como o CodeCaster observado acima. Mas dizendo que estou muito interessado em sua sugestão com relação à segurança. Você poderia sugerir o que devo fazer para evitar esse comportamento?
precisa saber é o seguinte
Atualizei minha resposta com informações adicionais sobre como evitar a vulnerabilidade de segurança.
Kaspars Ozols
13

Tente o seguinte:

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

para mim, a cópia local foi a fonte do problema. isso resolveu

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

Meu caso foi que eu não tinha acesso direto ao contexto EF do meu aplicativo MVC.

Portanto, se você estiver usando algum tipo de repositório para persistência da entidade, pode ser apropriado desanexar a entidade carregada explicitamente e definir EntityState vinculado como Modificado.

Código de amostra (abstrato):

MVC

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

Repositório

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

void Detach(A a)
{
   context.Entry(a).EntityState = EntityState.Detached;
}
sephirot
fonte
Isso funcionou para mim, embora eu não tenha me incomodado em usar um repositório para fazer referência aos estados da entidade de contexto.
Eckert
3

Pensei em compartilhar minha experiência com essa, apesar de me sentir um pouco boba por não ter percebido antes.

Estou usando o padrão de repositório com as instâncias de repositório injetadas nos meus controladores. Os repositórios concretos instanciam meu ModelContext (DbContext), que dura a vida útil do repositório, que é IDisposablee é descartado pelo controlador.

O problema para mim foi que eu tenho uma versão modificada de carimbo e linha em minhas entidades, então eu as estava obtendo primeiro para comparar com os cabeçalhos de entrada. Obviamente, isso carregou e acompanhou a entidade que estava sendo atualizada posteriormente.

A correção foi simplesmente mudar o repositório de atualizar um contexto uma vez no construtor para ter os seguintes 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

Isso permite que os métodos do repositório renovem sua instância de contexto a cada uso chamando GetDbContextou use uma instância anterior, se assim o desejarem, especificando true.

Luke Puplett
fonte
2

Eu adicionei esta resposta apenas porque o problema é explicado com base em padrões de dados mais complexos e achei difícil de entender aqui.

Eu criei um aplicativo bastante simples. Este erro ocorreu dentro da ação Editar POST. A ação aceitou o ViewModel como um parâmetro de entrada. O motivo para usar o ViewModel foi fazer algum cálculo antes de o registro ser salvo.

Depois que a ação passou pela validação, como if(ModelState.IsValid), meu erro foi projetar valores do ViewModel em uma instância completamente nova do Entity. Eu pensei que teria que criar uma nova instância para armazenar dados atualizados e salvá-la.

O que eu percebi mais tarde foi que eu tinha que ler o registro do banco de dados:

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

e atualizou este objeto. Tudo funciona agora.

Celdor
fonte
2

Eu tive esse problema com var local e eu apenas desanexá-lo assim:

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 problemáticas de objetos carregados com a mesma chave; portanto, primeiro desanexaremos esse objeto e faremos a atualização para evitar conflitos entre dois objetos com a mesma chave

lvl4fi4
fonte
@Artjom B Problema causa de objetos carregados com a mesma chave, então primeiro vamos separar esse objeto e fazer a atualização para o conflito evitar entre dois objetos com a mesma chave
lvl4fi4
2

Eu tive um problema semelhante, depois de pesquisar por 2-3 dias, encontrado ".AsNoTracking" deve ser removido, pois a EF não rastreia as alterações e assume que não há alterações, a menos que um objeto seja anexado. Além disso, se não usarmos .AsNoTracking, a EF saberá automaticamente qual objeto salvar / atualizar, para que não haja necessidade de usar Anexar / Adicionado.

Prem
fonte
2

Use AsNoTracking()onde você está obtendo sua consulta.

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

Encontrei este erro em que

  • dois métodos, A e B, em um único controlador, ambos usaram a mesma instância de um ApplicationDbContext e
  • método A chamado 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();
    }

Mudei o método B para ter uma instrução using e confiar apenas no db2 local . Depois de:

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

Semelhante ao que Luke Puplett está dizendo, o problema pode ser causado por não descartar ou criar adequadamente o seu contexto.

No meu caso, eu tive uma classe que aceitou um contexto chamado ContextService:

public class ContextService : IDisposable
{
    private Context _context;

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

Meu serviço de contexto tinha uma função que atualiza uma entidade usando um objeto de entidade 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();
        }

Tudo isso estava bem, meu controlador onde eu inicializei o serviço foi o problema. Meu controlador originalmente era assim:

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

Eu mudei para isso e o erro desapareceu:

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

Este problema também pode ser visto durante ViewModela EntityModelmapeamento (usando AutoMapper, etc.) e tentando incluem context.Entry().Statee context.SaveChanges()como um bloco utilizando como mostrado abaixo resolveria o problema. Por favor, lembre-se de que o context.SaveChanges()método é usado duas vezes em vez de usar logo após, if-blockpois também deve ser usado no bloco.

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 isto ajude...

Murat Yıldız
fonte
1

Aqui o que eu fiz no caso semelhante.

Essa situação significa que a mesma entidade já existia no contexto.

Primeiro verifique no ChangeTracker se a entidade está no contexto

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

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

Se existir

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

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

Eu mantenho para corrigir o problema, atualizando o estado. quando você aciona a localização ou qualquer outra operação de consulta no mesmo estado de registro foi atualizada com modificada, precisamos definir o status como Desanexado, para que você possa acionar sua alteração de atualização

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

Eu resolvo esse problema com um bloco "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)
{

    }

Aqui é onde eu recebo a ideia https://social.msdn.microsoft.com/Forums/sqlserver/es-ES/b4b350ba-b0d5-464d-8656-8c117d55b2af/problema-al-modificar-en-entity-framework?forum = vcses está em espanhol (procure a segunda resposta)

Suzume
fonte
tenha cuidado e use apenas 1 instância de conexão com o banco de dados, especialmente se você estiver usando o framework de entidades, se não o fizer, receberá o erro Entity Framework Um objeto de entidade não pode ser referenciado por várias instâncias de IEntityChangeTracker
Suzume
1

você pode usar um método adicionado como;

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

mas, em muitos casos, se você desejar usar mais de um modelo nesse momento, isso não funcionará porque a entidade já está anexada a outra entidade. Portanto, nesse momento, você pode usar o método de migração de entidades ADDOrUpdate, que simplesmente migra um objeto de um para outro e, como resultado, você não recebe nenhum erro.

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

Limpar tudo Estado

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

xxxsenatorxxx
fonte