Bob Tway
Bob Tway

Reputation: 9603

Entity Framework not saving modified children

Frustrating, this. Here's a pair of related objects, as generated by database-first Entity Framework:

public partial class DevelopmentType
{
    public DevelopmentType()
    {
        this.DefaultCharges = new HashSet<DefaultCharge>();
    }

    public System.Guid RowId { get; set; }
    public string Type { get; set; }

    public virtual ICollection<DefaultCharge> DefaultCharges { get; set; }
}

public partial class DefaultCharge
{
    public System.Guid RowId { get; set; }
    public decimal ChargeableRate { get; set; }
    public Nullable<System.Guid> DevelopmentType_RowId { get; set; }

    public virtual DevelopmentType DevelopmentType { get; set; }
}

Here's the code that I'm calling to save a DevelopmentType - it involves automapper since we differentiate entity objects from DTOs:

    public void SaveDevelopmentType(DevelopmentType_dto dt)
    {
        Entities.DevelopmentType mappedDevType = Mapper.Map<DevelopmentType_dto, Entities.DevelopmentType>(dt);
        _Context.Entry(mappedDevType).State = System.Data.EntityState.Modified;

        _Context.DevelopmentTypes.Attach(mappedDevType);
        _Context.SaveChanges();
    }

In my user interface, the most common operation will be for a user to look at a list of DevelopmentTypes and update their DefaultCharge. So when I test this using the above code, it runs without error, but nothing actually changes.

If I pause in the debugger it's clear that the changed DefaultCharge is being passed into the function, and that it's attached to the DevelopmentType to be saved.

Stepping through it, if I change the value manually inside visual studio, it does save the updated value. Which is just even more confusing.

Monitoring the database with SQL Server Profiler reveals that update commands are issued only for the parent object and not for any attached objects.

I have other similar code elsewhere that functions as expected. What am I doing wrong here?

EDIT:

I have discovered that if you do this prior to the call to SaveDevelopmentType:

        using (TransactionScope scope = new TransactionScope())
        {
            dt.Type = "Test1";
            dt.DefaultCharges.First().ChargeableRate = 99;
            _CILRepository.SaveDevelopmentType(dt);
            scope.Complete();
        }

The change to Type saves, but the change to ChargeableRate does not. I don't think it helps, massively, but thought I'd add it.

Upvotes: 28

Views: 48010

Answers (8)

Skattch
Skattch

Reputation: 9

This is an old question but as none of the answers above worked for me, I thought I would post what did.

Basically, I just stepped through the array of child items and set each to modified.

if (schema.Id > 0)
{
    SchemaModel? tempSchema = await _context.Schemas.FirstOrDefaultAsync(x => x.Id == schema.Id);
    if (tempSchema != null)
    {
        _context.Entry(tempSchema).CurrentValues.SetValues(schema);
        _context.Entry(tempSchema).State = EntityState.Modified;
    }
    if (schema.Columns != null)
    {
        foreach (var col in schema.Columns)
        {
            _context.Entry(col).State = EntityState.Modified;
        }
    }
}

Upvotes: 0

Bryan
Bryan

Reputation: 268

I created a helper method to solve this problem.


Consider this:

public abstract class BaseEntity
{
    /// <summary>
    /// The unique identifier for this BaseEntity.
    /// </summary>
    [Key]        
    public Guid Id { get; set; }
}

public class BaseEntityComparer : IEqualityComparer<BaseEntity>
{
    public bool Equals(BaseEntity left, BaseEntity right)
    {
        if (ReferenceEquals(null, right)) { return false; }
        return ReferenceEquals(left, right) || left.Id.Equals(right.Id);
    }

    public int GetHashCode(BaseEntity obj)
    {
        return obj.Id.GetHashCode();
    }
}

public class Event : BaseEntity
{
    [Required(AllowEmptyStrings = false)]
    [StringLength(256)]
    public string Name { get; set; }
    public HashSet<Manager> Managers { get; set; }
}

public class Manager : BaseEntity
{
    [Required(AllowEmptyStrings = false)]
    [StringLength(256)]
    public string Name { get; set; }
    public Event Event{ get; set; }
}

DbContext with the helper method:

public class MyDataContext : DbContext
{
    public MyDataContext() : base("ConnectionName") { }

    //Tables
    public DbSet<Event> Events { get; set; }
    public DbSet<Manager> Managers { get; set; }

    public async Task AddOrUpdate<T>(T entity, params string[] ignoreProperties) where T : BaseEntity
    {
        if (entity == null || Entry(entity).State == EntityState.Added || Entry(entity).State == EntityState.Modified) { return; }
        var state = await Set<T>().AnyAsync(x => x.Id == entity.Id) ? EntityState.Modified : EntityState.Added;
        Entry(entity).State = state;

        var type = typeof(T);
        RelationshipManager relationship;
        var stateManager = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager;
        if (stateManager.TryGetRelationshipManager(entity, out relationship))
        {
            foreach (var end in relationship.GetAllRelatedEnds())
            {
                var isForeignKey = end.GetType().GetProperty("IsForeignKey", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(end) as bool?;
                var navigationProperty = end.GetType().GetProperty("NavigationProperty", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(end);
                var propertyName = navigationProperty?.GetType().GetProperty("Identity", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(navigationProperty) as string;
                if (string.IsNullOrWhiteSpace(propertyName) || ignoreProperties.Contains(propertyName)) { continue; }

                var property = type.GetProperty(propertyName);
                if (property == null) { continue; }

                if (end is IEnumerable) { await UpdateChildrenInternal(entity, property, isForeignKey == true); }
                else { await AddOrUpdateInternal(entity, property, ignoreProperties); }
            }
        }

        if (state == EntityState.Modified)
        {
            Entry(entity).OriginalValues.SetValues(await Entry(entity).GetDatabaseValuesAsync());
            Entry(entity).State = GetChangedProperties(Entry(entity)).Any() ? state : EntityState.Unchanged;
        }
    }

    private async Task AddOrUpdateInternal<T>(T entity, PropertyInfo property, params string[] ignoreProperties)
    {
        var method = typeof(EasementDataContext).GetMethod("AddOrUpdate");
        var generic = method.MakeGenericMethod(property.PropertyType);
        await (Task)generic.Invoke(this, new[] { property.GetValue(entity), ignoreProperties });
    }

    private async Task UpdateChildrenInternal<T>(T entity, PropertyInfo property, bool isForeignKey)
    {
        var type = typeof(T);
        var method = isForeignKey ? typeof(EasementDataContext).GetMethod("UpdateForeignChildren") : typeof(EasementDataContext).GetMethod("UpdateChildren");
        var objType = property.PropertyType.GetGenericArguments()[0];
        var enumerable = typeof(IEnumerable<>).MakeGenericType(objType);

        var param = Expression.Parameter(type, "x");
        var body = Expression.Property(param, property);
        var lambda = Expression.Lambda(Expression.Convert(body, enumerable), property.Name, new[] { param });
        var generic = method.MakeGenericMethod(type, objType);

        await (Task)generic.Invoke(this, new object[] { entity, lambda, null });
    }

    public async Task UpdateForeignChildren<T, TProperty>(T parent, Expression<Func<T, IEnumerable<TProperty>>> childSelector, IEqualityComparer<TProperty> comparer = null) where T : BaseEntity where TProperty : BaseEntity
    {
        var children = (childSelector.Invoke(parent) ?? Enumerable.Empty<TProperty>()).ToList();
        foreach (var child in children) { await AddOrUpdate(child); }

        var existingChildren = await Set<T>().Where(x => x.Id == parent.Id).SelectMany(childSelector).AsNoTracking().ToListAsync();

        if (comparer == null) { comparer = new BaseEntityComparer(); }
        foreach (var child in existingChildren.Except(children, comparer)) { Entry(child).State = EntityState.Deleted; }
    }

    public async Task UpdateChildren<T, TProperty>(T parent, Expression<Func<T, IEnumerable<TProperty>>> childSelector, IEqualityComparer<TProperty> comparer = null) where T : BaseEntity where TProperty : BaseEntity
    {
        var stateManager = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager;
        var currentChildren = childSelector.Invoke(parent) ?? Enumerable.Empty<TProperty>();
        var existingChildren = await Set<T>().Where(x => x.Id == parent.Id).SelectMany(childSelector).AsNoTracking().ToListAsync();

        if (comparer == null) { comparer = new BaseEntityComparer(); }
        var addedChildren = currentChildren.Except(existingChildren, comparer).AsEnumerable();
        var deletedChildren = existingChildren.Except(currentChildren, comparer).AsEnumerable();

        foreach (var child in currentChildren) { await AddOrUpdate(child); }
        foreach (var child in addedChildren) { stateManager.ChangeRelationshipState(parent, child, childSelector.Name, EntityState.Added); }
        foreach (var child in deletedChildren)
        {
            Entry(child).State = EntityState.Unchanged;
            stateManager.ChangeRelationshipState(parent, child, childSelector.Name, EntityState.Deleted);
        }
    }

    public static IEnumerable<string> GetChangedProperties(DbEntityEntry dbEntry)
    {
        var propertyNames = dbEntry.State == EntityState.Added ? dbEntry.CurrentValues.PropertyNames : dbEntry.OriginalValues.PropertyNames;
        foreach (var propertyName in propertyNames)
        {
            if (IsValueChanged(dbEntry, propertyName))
            {
                yield return propertyName;
            }
        }
    }

    private static bool IsValueChanged(DbEntityEntry dbEntry, string propertyName)
    {
        return !Equals(OriginalValue(dbEntry, propertyName), CurrentValue(dbEntry, propertyName));
    }

    private static string OriginalValue(DbEntityEntry dbEntry, string propertyName)
    {
        string originalValue = null;

        if (dbEntry.State == EntityState.Modified)
        {
            originalValue = dbEntry.OriginalValues.GetValue<object>(propertyName) == null
                ? null
                : dbEntry.OriginalValues.GetValue<object>(propertyName).ToString();
        }

        return originalValue;
    }

    private static string CurrentValue(DbEntityEntry dbEntry, string propertyName)
    {
        string newValue;

        try
        {
            newValue = dbEntry.CurrentValues.GetValue<object>(propertyName) == null
                ? null
                : dbEntry.CurrentValues.GetValue<object>(propertyName).ToString();
        }
        catch (InvalidOperationException) // It will be invalid operation when its in deleted state. in that case, new value should be null
        {
            newValue = null;
        }

        return newValue;
    }
}

Then I call it like this

    // POST: Admin/Events/Edit/5
    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Edit(Event @event)
    {
        if (!ModelState.IsValid) { return View(@event); }

        await _db.AddOrUpdate(@event);
        await _db.SaveChangesAsync();

        return RedirectToAction("Index");
    }

Upvotes: 1

user6435807
user6435807

Reputation:

If I understand the question correctly, you have problem updating child fields. I had problems with child collection fields. I tried this and it worked for me. You should update all child collections after attaching the object to the database context change the modified state of the parent object and save changes to the context.

Database.Products.Attach(argProduct);
argProduct.Categories = Database.Categories.Where(x => ListCategories.Contains(x.CategoryId)).ToList();
Database.Entry(argProduct).State = EntityState.Modified;
Database.SaveChanges();

Upvotes: 1

Javier
Javier

Reputation: 2159

The Graphdiff library was a great help for me to handle all of these complexities.

You only need to set up the navigation properties that you wish to insert/update/delete (using fluent syntax) and Graphdiff will take care of it

Note: It seems to be that the project is not updated anymore but i'm using it since more than a year and is quite stable

Upvotes: 3

haim770
haim770

Reputation: 49095

Context.Entry() already "Attaches" the Entity internally in order to have the context change its EntityState.

By calling Attach() you're changing the EntityState back to Unchanged. Try to comment out this line.

Upvotes: 6

jocull
jocull

Reputation: 21105

This is not a workaround for every case, but I did discover that you can get around this by updating foreign keys on an object instead of updating navigation property objects.

For example... instead of:

myObject.myProperty = anotherPropertyObject;

Try this:

myObject.myPropertyID = anotherPropertyObject.ID;

Make sure the object is flagged as modified in EF's mind (as mentioned in other posts) and then call your save method.

Worked for me at least! It'll be a no-go when working with nested properties, but perhaps you can break your contexts up into smaller chunks and work over objects in multiple parts to avoid context bloat.

Good luck! :)

Upvotes: 1

boindiil
boindiil

Reputation: 5865

The problem is, that EF is not aware of the changed DefaultCharges.

By setting the State of the DevelopmentType to EntityState.Modified, EF only knows that the object DevelopmentType has been changed. However, this means that EF will only update DevelopmentType but not it's navigation properties.

A workaround - which isn't best practice - would be to iterate over all DefaultCharge of the current DevelopmentType and set the entity state to EntityState.Modified.

Additionally I would recommend to attach the entity to the context first, and change the state afterwards.

EDIT after comment

As you are using DTOs I suppose you are transfering these objects either through different layers or different machines.

In this case I would recommend to use self tracking entities, because it is not possible to share one context. These entities additionally holds their current state (ie. new, updated, deleted etc). There are many tutorials on the net about self tracking entities.

e.g. MSDN - Working with Self-Tracking Entities

Upvotes: 25

JTMon
JTMon

Reputation: 3199

As far as I know EF can save child entities only if the parent object was retrieved with the same Context that is trying to save it. That is attaching an object that was retrieved by one context to another context, will allow you to save changes to parent objects but not children. This was the result of a on old search based on which we switched to NHibernate. If memory serves correctly I was able to find a link where EF team member(s) confirmed this and that there WAS no plan to change this behavior. Unfortunately all links related to that search have been erased from my PC since.

As I am not aware of how you are retrieving the objects in your case, I am not sure this is relevant to your case, but put it out there just in case it helps.

Here is a link on attaching detached objects to a context.

http://www.codeproject.com/Articles/576330/Attaching-detached-POCO-to-EF-DbContext-simple-and

Upvotes: 6

Related Questions