Update Entity Framework 6 Objects disconnected Entities 1: N (One to many)

1

I'm making an application and I use Entity Framework 6 to access SQL Server.

I have the following case, where I want to update the value of two entities:

Socio - > SocioDireccion A partner can have many addresses.

First I call this method to read a partner:

public Socio Leer(int id)
{
    Socio resultado = null;

    using (var context = new BDConectaClubContext())
    {
        context.Configuration.LazyLoadingEnabled = false;
        context.Configuration.ProxyCreationEnabled = false;

        //Usando Include linq lambda

        resultado = context.Socio
            .Include(s => s.CuentaBancaria)
            .Include(s=> s.Direcciones)
            .Include(s=>s.EMails)
            .Include(s=>s.Grupos)
            .Include(s=>s.Vocalias)
            .Where(s => s.Id == id).FirstOrDefault<Socio>();
    }


    return resultado;

}

In the database, the read Partner has only one address. I add two more directions, out of context, since they are disconnected entities, being as follows:

Socio.
----------------
Id
38 (Modificación)


SocioDireccion
---------------
Id    Socio_Id
1     38 (Modificación)
0     38 (Alta)
0     38 (Alta)

I call the following method that helps me to add or modify a partner:

public bool Guardar(Socio socio)
{
    int r; 
    using (var ctx = new BDConectaClubContext())
    {

        ctx.Configuration.ProxyCreationEnabled = false;
        ctx.Configuration.LazyLoadingEnabled = false;

        // Alta funciona correctamente
        if (socio.Id == 0)
        {
            ctx.Entry(socio).State = EntityState.Added;
            foreach (SocioDireccion d in socio.Direcciones)
            {
                ctx.Entry(d).State = EntityState.Added;
            }
            r = ctx.SaveChanges();
        }
        else
        {
            //Aquí se produce el error
            ctx.Entry(socio).State = EntityState.Modified;             


            foreach (SocioDireccion d in socio.Direcciones)
            {
                if (d.Id == 0)
                    ctx.Entry(d).State = EntityState.Added;
                else
                    ctx.Entry(d).State = EntityState.Modified;
            }

            r = ctx.SaveChanges();
        }


        return r > 0;
    }
}

When this line is running

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

... the following error occurs:

  

Attaching an entity of type 'RMG.ConectaClub.Models.SocioDirection' failed because another entity of the same type has already the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.

The funny thing is that if I just add a new address to SocioDireccion the error does not occur and the modification works correctly:

Socio
-------
Id
38 (Modificación)


SocioDireccion
---------------------
Id Socio_Id
1 38 (Modificación)
0 38 (Alta) 

With only one new address the error does not occur. If there is more than one, the error occurs.

I've done a simple example so you can see how I add the information to the entities:

private void button1_Click_1(object sender, EventArgs e)
{
    //Crear un nuevo socio llamada Pepe con una direccion a
    Socio s = new Socio();
    s.Nombre = "Pepe";

    SocioDireccion direccion = new SocioDireccion();
    direccion.Direccion = "calle a";
    direccion.Socio = s;

    s.Direcciones.Add(direccion);

    new FachadaSocio().Guardar(s);


    //Leer el socio Pepe y crear dos direcciones a y b
    Socio nuevoSocio = new FachadaSocio().Leer(s.Id);

    SocioDireccion dir;
    dir = new SocioDireccion();
    dir.Direccion = "calle b";
    dir.Socio = nuevoSocio;

    nuevoSocio.Direcciones.Add(dir);

    dir = new SocioDireccion();
    dir.Direccion = "calle c";
    dir.Socio = nuevoSocio;
    nuevoSocio.Direcciones.Add(dir);

    // El socio Pepe tiene 3 direcciones a,b,c
    new FachadaSocio().Guardar(nuevoSocio);

}

I add more information:

The error may be related to this that I have detected. Can you tell me if it's working correctly?

I go through the list of addresses. When an address has the Id=0 , I change the state to Added . But you can see, when doing an inspection, that I have added all the other entities of Addresses and also the entity Partner.

foreach (SocioDireccion d in socio.Direcciones)
{                        
    estadoDireccion = ctx.Entry(d).State;

    if (d.Id == 0)
        // Al ejecutar esta instrucción añade el resto de Direcciones y la entidad Socio.
        ctx.Entry(d).State = EntityState.Added; 
    else
        ctx.Entry(d).State = EntityState.Modified;
}

ctx.Entry(d).State = EntityState.Added; adds the entire address list to the context and also adds the Partner to the context.

Acts exactly as if you were adding a parent entity that adds all the daughters with the added state.

Any ideas? Thank you very much for your answers.

    
asked by Ram 23.11.2016 в 10:38
source

2 answers

1

The problem with:

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

... is that not only does it change the State from socio to Modified , but it also goes through all the addresses and if it finds one that is Detached , it changes the State to Unchanged automatically.

Now, Unchanged represents an entity that already exists in the database. But in the case that gives you an error, the truth is that you have 2 addresses that do not yet exist in the database, so to put the state Unchanged in fact is a lie and is not correct.

With a single new address, the lie goes unnoticed, and anyway you correct the State to Added or Modified and nothing happens.

But when you have at least 2 new addresses, now the lie is felt, because EF notes 2 entities that supposedly already exist in the database, but share the same primary key ( Id = 0 ), and that does not like it, so it throws the error.

The solution is simple: change the State of the directions before to change the State from socio to Modified . In this way, by the time you execute the problematic statement, the addresses will no longer be in State = Detached , and EF will not try to put the State to Unchanged , which would be incorrect.

Code:

else
{
    foreach (SocioDireccion d in socio.Direcciones)
    {
        if (d.Id == 0)
            ctx.Entry(d).State = EntityState.Added;
        else
            ctx.Entry(d).State = EntityState.Modified;
    }

    // Mueve esta sentencia después de las direcciones.
    ctx.Entry(socio).State = EntityState.Modified;             

    r = ctx.SaveChanges();
}

Edit

I'm sorry you still have the problem despite reordering the statements to modify the State of the addresses before the partner. It seems that I underestimated the way EF modifies the State automatically following the navigation properties .

I think what is happening is the following:

  • You execute the following statement: ctx.Entry(d).State = EntityState.Modified;
  • EF notes that the address is related to the instance socio that is still Detached , so it changes it to Unchanged .
  • Starting from the instance socio , EF finds the other addresses and notes that they are at Detached , so try to change them to Unchanged before you have the opportunity to change the State yourself to Modified , which causes the error, because you have more than one address with Id = 0 .
  • Finally, although my explanation is not understood at all, I propose a modification, which now, I think will solve the problem.

    I think that even if you're in update mode, you should change the State from socio to Added . This has the effect (as you already noted) of changing the State of all the addresses to Added , whose State does not cause problems with Id = 0 .

    The next step is to modify the State of the addresses to Added or Modified as you already do.

    And finally, the final stage: you change the State to the instance socio again, but this time to Modified .

    Code:

    else
    {
        // 1. Asigna el state "Added" temporalmente para eliminar el error.
        ctx.Entry(socio).State = EntityState.Added;             
    
        // 2. Asigna los states a tus direcciones normalmente.
        foreach (SocioDireccion d in socio.Direcciones)
        {
            if (d.Id == 0)
                ctx.Entry(d).State = EntityState.Added;
            else
                ctx.Entry(d).State = EntityState.Modified;
        }
    
        // 3. Ahora sí le cambias el state a "Modified" sin problemas.
        ctx.Entry(socio).State = EntityState.Modified;             
    
        r = ctx.SaveChanges();
    }
    

    Final note

    In fact, the fact that the code is so complicated is a sign that the design is not the best. Actually, EF does not work very well with disconnected entities, particularly when there are several related entities.

    Of being in your place, I would seriously think of working with entities "connected" to a context that does not close until you finish working with the entities. If you try, you will see that the code is reduced and simplified, and works more intuitively.

    Based on the existing code you have, here is an example of code using "contectas" entities. Note that the Guardar disappears completely:

    Method Leer :

    public Socio Leer(int id, DbContext context)
    {
            //Usando Include linq lambda
            return context.Socio
                .Include(s => s.CuentaBancaria)
                .Include(s=> s.Direcciones)
                .Include(s=>s.EMails)
                .Include(s=>s.Grupos)
                .Include(s=>s.Vocalias)
                .Where(s => s.Id == id).FirstOrDefault();
    }
    

    Event handler:

    private void button1_Click_1(object sender, EventArgs e)
    {
        Socio s;
        using (var ctx = new BDConectaClubContext())
        {
            ctx.Configuration.ProxyCreationEnabled = false;
            ctx.Configuration.LazyLoadingEnabled = false;
    
            //Crear un nuevo socio llamada Pepe con una direccion a
            s = new Socio();
            s.Nombre = "Pepe";
    
            SocioDireccion direccion = new SocioDireccion();
            direccion.Direccion = "calle a";
            direccion.Socio = s;
    
            s.Direcciones.Add(direccion);
    
            ctx.SaveChanges();
        }
    
        using (var ctx = new BDConectaClubContext())
        {
            //Leer el socio Pepe y crear dos direcciones a y b
            Socio nuevoSocio = new FachadaSocio().Leer(s.Id, ctx);
    
            SocioDireccion dir;
            dir = new SocioDireccion();
            dir.Direccion = "calle b";
            dir.Socio = nuevoSocio;
    
            nuevoSocio.Direcciones.Add(dir);
    
            dir = new SocioDireccion();
            dir.Direccion = "calle c";
            dir.Socio = nuevoSocio;
            nuevoSocio.Direcciones.Add(dir);
    
            // El socio Pepe tiene 3 direcciones a,b,c
            ctx.SaveChanges();
        }
    }
    
        
    answered by 24.11.2016 в 03:08
    0

    Try this way:

            //Aquí se produce el error
            ctx.Entry(socio).State = EntityState.Modified;             
    
    
            foreach (SocioDireccion d in socio.Direcciones)
            {
                if (d.Id == 0)
                    ctx.Entry(d).State = EntityState.Added;
                else
                    {
                        // Uliza el método Find de tú DbSet Direcciones (cro que se llama así)
                        var data = context.Direcciones.Find(d.Id);
                        var entry = context.Entry(data);
                        entry.CurrentValues.SetValues(entity);
                    }
            }
    
            r = ctx.SaveChanges();
    

    It's another way to make changes, and that's how it should work correctly.

    I leave you a generic AddOrUpdate method, which can be of great help in these cases:

    public virtual int InsertOrUpdate<TEntity>(TEntity entity, params object[] keyValues)  where TEntity : class
    {
        if (entity == null) throw new ArgumentNullException(nameof(entity), $"El parámetro {nameof(entity)} no puede ser null");
        if (keyValues == null) throw new ArgumentNullException(nameof(keyValues), $"El parámetro {nameof(keyValues)} no puede ser null");
    
        int resultado = 0;
    
        var context = new TuContexto(); //CreateContext();
    
        var dbSet = context.Set<TEntity>();
    
        var data = dbSet.Find(keyValues);
    
        if (data == null)
        {
            dbSet.Attach(entity);
            context.Entry(entity).State = EntityState.Added;
        }
        else
        {
            var entry = context.Entry(data);
            entry.CurrentValues.SetValues(entity);
        }
    
        resultado = TuContexto.SaveChanges();
    
        TuContexto.Dispose(); //DisposeContext();
    
        return resultado;
    }
    

    I leave a little creation and destruction of the context to the consumer's liking, in case you want to insert it in the constructor of the same or in the same method or wherever you want.

        
    answered by 23.11.2016 в 11:08