Entity Framework не сохраняет измененных детей

Разочарование, это. Здесь пара связанных объектов, созданных базой данных 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; }
}

Здесь код, который я вызываю, чтобы сохранить DevelopmentType - он включает automapper, поскольку мы выделяем объекты сущности из DTO:

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

В моем пользовательском интерфейсе наиболее распространенной операцией будет просмотр пользователем списка DevelopmentTypes и обновление их DefaultCharge. Поэтому, когда я тестирую это, используя приведенный выше код, он работает без ошибок, но ничего не меняется.

Если я приостанавливаюсь в отладчике, он очищается от того, что измененная команда DefaultCharge передается в функцию и что она привязана к типу DevelopmentType для сохранения.

Пройдя через него, если я изменил значение вручную внутри visual studio, он сохранит обновленное значение. Это еще более запутанно.

Мониторинг базы данных с помощью SQL Server Profiler показывает, что команды обновления выдаются только для родительского объекта, а не для всех подключенных объектов.

У меня есть другой аналогичный код в другом месте, который функционирует так, как ожидалось. Что я здесь делаю неправильно?

EDIT:

Я обнаружил, что если вы сделаете это до вызова SaveDevelopmentType:

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

Изменение типа сохраняет, но изменения в ChargeableRate нет. Я не думаю, что это помогает, в массовом порядке, но я думал, что добавлю его.

Ответ 1

Проблема заключается в том, что EF не знает об изменениях DefaultCharges.

Установив состояние DevelopmentType на EntityState.Modified, EF знает только, что объект DevelopmentType был изменен. Однако это означает, что EF будет обновлять только DevelopmentType, но не навигационные свойства.

Обходной путь - это не самая лучшая практика - это перебрать все DefaultCharge текущего DevelopmentType и установить состояние объекта EntityState.Modified.

Кроме того, я бы рекомендовал сначала привязать объект к контексту и после этого изменить состояние.

ИЗМЕНИТЬ после комментария

Поскольку вы используете DTO, я предполагаю, что вы переносите эти объекты либо через разные уровни, либо на разные машины.

В этом случае я бы рекомендовал использовать объекты самостоятельного отслеживания, потому что невозможно разделить один контекст. Эти сущности дополнительно содержат свое текущее состояние (то есть новое, обновленное, удаленное и т.д.). В сети много обучающих программ, посвященных объектам самостоятельного отслеживания.

например. MSDN - Работа с объектами самопроверки

Ответ 2

Context.Entry() уже "привязывает" Entity внутри, чтобы изменить контекст EntityState.

Вызывая Attach(), вы меняете EntityState на Unchanged. Попробуйте прокомментировать эту строку.

Ответ 3

Насколько я знаю, EF может сохранять дочерние сущности только в том случае, если родительский объект был восстановлен с тем же Контекстом, который пытается его сохранить. Это привязка объекта, который был получен одним контекстом в другом контексте, позволит вам сохранять изменения в родительских объектах, но не для детей. Это было результатом старого поиска, на основе которого мы перешли на NHibernate. Если память правильно работает, мне удалось найти ссылку, в которой члены (члены) EF подтвердили это, и что там не было никакого плана изменить это поведение. К сожалению, все ссылки, связанные с этим поиском, были удалены с моего ПК с тех пор.

Поскольку я не знаю, как вы извлекаете объекты в своем случае, я не уверен, что это имеет отношение к вашему делу, но поместите его там на всякий случай, если это поможет.

Вот ссылка на прикрепление отдельных объектов к контексту.

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

Ответ 4

Библиотека Graphdiff очень помогла мне справиться со всеми этими сложностями.

Вам нужно только настроить свойства навигации, которые вы хотите вставить/обновить/удалить (используя свободный синтаксис), и Graphdiff позаботится об этом

Примечание. Кажется, что проект больше не обновляется, но я использую его с более чем года и довольно стабилен.

Ответ 5

Это не обходной путь для каждого случая, но я обнаружил, что вы можете обойти это, обновив внешние ключи для объекта, вместо обновления объектов свойств навигации.

Например... вместо:

myObject.myProperty = anotherPropertyObject;

Попробуйте следующее:

myObject.myPropertyID = anotherPropertyObject.ID;

Убедитесь, что объект отмечен как измененный в умении EF (как указано в других сообщениях), а затем вызовите метод сохранения.

Работал для меня хотя бы! При работе с вложенными свойствами это будет нехорошо, но, возможно, вы можете разбить свои контексты на более мелкие куски и работать над объектами в нескольких частях, чтобы избежать раздувания контекста.

Удачи!:)

Ответ 6

Если я правильно понял вопрос, у вас возникли проблемы с обновлением дочерних полей. У меня были проблемы с полями сбора детей. Я попробовал это, и это сработало для меня. Вы должны обновить все дочерние коллекции после присоединения объекта к контексту базы данных, изменить измененное состояние родительского объекта и сохранить изменения в контексте.

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

Ответ 7

Я создал вспомогательный метод для решения этой проблемы.


Рассмотрим это:

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 со вспомогательным методом:

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

Тогда я называю это следующим образом

    // 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");
    }