Attach many objects with id = 0

Asked

Viewed 74 times

2

Good morning Personal,

In my system here at work I have the following architecture: A PROJECT (which has some information) is composed of several PARTS. The parts are very different from each other. One, for example part 1, has several attached documents.

The Projects view has been assembled with a Tab Panel (a tab for each part) so that the user can change several parts at once.

In the Project controller, I get the Project model from the view, with all its changes and inclusions. And I start to analyze it:

    private void salvar(Projetos projeto)
    {
        try
        {
            resolverParte1(projeto);
            resolverParte2(projeto);
            ...

            ctx.Entry(projeto).State = EntityState.Modified;
            ctx.SaveChanges();
        }
        catch (Exception ex)
        {

        }
  }

  private void resolverParte1(Projetos projeto)
  {

        foreach(var d in projeto.Documentos.ToList())
        {
            // Documento alterado
            if(d.id != 0)
            {
                 ctx.Entry(d).State = EntityState.Modified;
            }
            else // Documento adicionado
            {
                 d.Parte1Id = parte1Id;
                 ctx.Entry(d).State = EntityState.Added;
            }
         }

         ctx.Projetos.Attach(projeto);

         ICollection<Documentos> dLista = null;

         ctx.Entry(projeto).Collection("Documentos").Load();
         dLista = projeto.Documentos;

         // A lista de documentos que permaneceu no banco como Unchanged é porque foi excluída na view
         var apagados = (from d in dLista where ctx.Entry(d).State = EntityState.Unchanged select d).ToList();

         foreach(var a in apagados)
         {
              ctx.Entry(a).State = EntityState.Deleted;
         }
    }

The problem I’m having is that by adding two or more documents (D1 and D2), they arrive at the controller with id = 0 (because I’m adding) and when running the attach line, the following Exception occurs "An Object with the same key already exists in the Objectstatemanager. The Objectstatemanager cannot track Multiple Objects with the same key".

Please, someone would know how to fix this?

Thank you very much.

1 answer

2


The problem is very similar with this reply, in which the questioner makes the same mistake, but does not deserve to be treated as duplicated because, in your case, the answer deserves further consideration.

For example, this part:

    foreach(var d in projeto.Documentos.ToList())
    {
        // Documento alterado
        if(d.id != 0)
        {
             ctx.Entry(d).State = EntityState.Modified;
        }
        else // Documento adicionado
        {
             d.Parte1Id = parte1Id;
             ctx.Entry(d).State = EntityState.Added;
        }
     }

     ctx.Projetos.Attach(projeto);

Like projeto.Documentos implements ICollection, projeto.Documentos.ToList() is unnecessary. The excerpt can be rewritten as:

foreach(var d in projeto.Documentos) { ... }

Another thing is about aggregated entities. From Entity Framework 6, this block foreach is unnecessary, as the context already attempts to resolve changes, including in dependent entities. That is, the whole block can be replaced by:

ctx.Entry(projeto).State = EntityState.Modified;
ctx.SaveChanges();

Also, the statement below:

ctx.Projetos.Attach(projeto);

Is incorrect at this point in the code. According to the official documentation itself, Attach should be used when you know two things:

  • The object has not yet been loaded in context;
  • The object exists in the bank.

Not the case here. projeto has been loaded before. The correct is to use:

ctx.Entry(projeto).State = EntityState.Modified;

Another troubling detail is here:

    // A lista de documentos que permaneceu no banco como Unchanged é porque foi excluída na view
     var apagados = (from d in dLista where ctx.Entry(d).State = EntityState.Unchanged select d).ToList();

     foreach(var a in apagados)
     {
          ctx.Entry(a).State = EntityState.Deleted;
     }

Here you mark the original records as EntityState.Deleted but only calls ctx.SaveChanges() in the parent function, which is, from a transactional point of view, not only incorrect but also bad practice.

To include a transactional scope in your code, use:

private void salvar(Projetos projeto)
{
    try
    {
        using (var scope = new TransactionScope()) 
        {
            resolverParte1(projeto);
            resolverParte2(projeto);
            ...

            ctx.Entry(projeto).State = EntityState.Modified;
            ctx.SaveChanges();

            scope.Complete();
        } 
    }
    catch (Exception ex)
    {

    }
}

If you make a point of keeping the logic for part resolution separate, the logic may receive nested transactional scope:

private void resolverParte1(Projetos projeto)
{
     using (var scope = new TransactionScope()) 
     {
         ctx.Entry(projeto).State = EntityState.Modified;
         ctx.SaveChanges();

         ICollection<Documentos> dLista = null;

         ctx.Entry(projeto).Collection("Documentos").Load();
         dLista = projeto.Documentos;

         // A lista de documentos que permaneceu no banco como Unchanged é porque foi excluída na view
         var apagados = (from d in dLista where ctx.Entry(d).State = EntityState.Unchanged select d).ToList();

         foreach(var a in apagados)
         {
             ctx.Entry(a).State = EntityState.Deleted;
         }

         scope.Complete();
    }
}

Browser other questions tagged

You are not signed in. Login or sign up in order to post.