Attach many objects with id = 0

2

Good morning, people,

On 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.

Projects view was assembled with a tab panel (one tab for each part) so that the user can change various parts at once.

In the Projects controller, I get the Project template from the view, with all its changes and inclusions. And start analyzing 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 when adding two or more documents (d1 and d2), they arrive at the controller with id = 0 (because I'm adding) and run the line attach occurs the following exception "An object with the same key already exists in the ObjectStateManager. The ObjectStateManager can not track multiple objects with the same key" .

Please, would anyone know how to solve this?

Thank you very much.

    
asked by anonymous 08.06.2015 / 15:34

1 answer

2

The problem is very similar with this answer I gave, where the questioner has the same error , but does not deserve to be treated as duplicate because, in your case, the answer deserves additional considerations.

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

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

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

Another thing is about aggregate entities. From Entity Framework 6, this foreach block is unnecessary, since the context already tries to resolve changes, even, in the 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);

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

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

Not the case here. projeto has already been loaded before. The correct one 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 call ctx.SaveChanges() in the parent function, which is, from the 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 are keen to maintain logic for solving separate parts, 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();
    }
}
    
08.06.2015 / 16:17