Oswaldo
Oswaldo

Reputation: 3376

EF Update Many-to-Many in Detached Scenario

I was trying to create a generic method to update an Entity and all it's collection properties from a detached object. For example:

public class Parent{
    public int Id { get; set; }
    public string ParentProperty { get; set; }
    public List<Child> Children1 { get; set; }
    public List<Child> Children2 { get; set; }
}

public class Child{
    public int Id { get; set; }
    public string ChildProperty { get; set; }
}

So, my first intention was to use something like this:

Repository<Parent>.Update(parentObj);

It would be perfect have a magic inside this method that update Parent properties and compare the list of Children of the parentObj to the current values in database and add/update/remove them accordingly, but it's too complex to my knowledge about EF/Reflection/Generic... and so I tried a second more easier way like this:

Repository<Parent>.Update(parentObj, parent => parent.Children1
                                     parent => parent.Children2);

This method would be a little harder to use, but yet acceptable. But how I think the second parameter had to be params Expression<Func<TEntity, ICollection<TRelatedEntity>>>[] relatedEntities I had problems to specify multiple TRelatedEntity. So my try was to 3rd step with no success yet...

Now I tried to call a method to update Parent and a sequence of methods to update Childreen, like this:

Repository<Parent>.Update(parentObj);
Repository<Parent>.UpdateChild(parentObj, parent => parent.Id, parent => parent.Children1);
Repository<Parent>.UpdateChild(parentObj, parent => parent.Id, parent => parent.Children2);

And the code:

public virtual void Update(TEntity entityToUpdate)
{
    context.Entry(entityToUpdate).State = EntityState.Modified;
}

public virtual void UpdateChild<TRelatedEntity>(TEntity entityToUpdate, Func<TEntity, object> keySelector, Expression<Func<TEntity, ICollection<TRelatedEntity>>> relatedEntitySelector) where TRelatedEntity: class
{
    var entityInDb = dbSet.Find(keySelector.Invoke(entityToUpdate));

    var current = relatedEntitySelector.Compile().Invoke(entityToUpdate);
    var original = relatedEntitySelector.Compile().Invoke(entityInDb);

    foreach (var created in current.Except(original))
    {
        context.Set<TRelatedEntity>().Add(created);
    }

    foreach (var removed in original.Except(current))
    {
        context.Set<TRelatedEntity>().Remove(removed);
    }

    foreach (var updated in current.Intersect(original))
    {
        context.Entry(updated).State = EntityState.Modified;
    }

    context.Entry(entityInDb).State = EntityState.Detached;
}

First problem was to get original values, because when I call dbSet.Find the entity is already in context (context.Entry(entityToUpdate).State = EntityState.Modified;).

So I tried to change order calling first Child:

Repository<Parent>.Update(parentObj);
Repository<Parent>.UpdateChild(parentObj, parent => parent.Id, parent => parent.Children1);
Repository<Parent>.UpdateChild(parentObj, parent => parent.Id, parent => parent.Children2);

And now I have the error:

Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=472540 for information on understanding and handling optimistic concurrency exceptions.

In summary, it would be very nice the first way, but I would be satisfied with the second/third too.

Thanks very much

Edit 1

Please, I need a native solution or using Automapper (which we already use in the project), because my customer don't like external dependencies and if we need to adapt something to the project, like working with Attached objects to update their related entities, so GraphDiff mencioned in the comments doesn't fit our needs (and VS 2015 RC crashed when I tried to install the package for tests)

Upvotes: 2

Views: 454

Answers (1)

Francisco Goldenstein
Francisco Goldenstein

Reputation: 13767

Have you considered getting the object from the DB and using AutoMapper to modify all the property values?

I mean:

var obj = GetObjectFromDB(...); 
AutoMapObj(obj, modifiedObj); 
SaveInDb();

Upvotes: 1

Related Questions