DbContext отменяет изменения без удаления

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

Эти модальные окна используют PropertyGrid для отображения свойств объекта, а также имеют кнопки отмены. Если какие-либо данные изменены и нажата кнопка отмены, изменения отражаются в родительской форме (где я не могу удалить DbContext object).

Есть ли способ отменить любые сделанные изменения, если метод DbContext.SaveChanges() НЕ был вызван?

ОБНОВЛЕНИЕ: Entity Framework версии 4.4.


person Raheel Khan    schedule 08.05.2013    source источник
comment
Приложение не сохраняет объект DbContext в течение всего времени его существования. Редактирование иерархического объекта также является единицей работы, требующей редактирования дочерних объектов. В моем случае я застрял с модальными окнами и подключенными/присоединенными объектами.   -  person Raheel Khan    schedule 08.05.2013
comment
Используйте DTO (или клон редактируемого объекта) в модальном окне. Когда редактирование отменено, просто отмените DTO, и с исходным объектом ничего не произойдет. Если вы хотите сохранить, сначала скопируйте значения DTO в исходный объект и сохраните изменения.   -  person Gert Arnold    schedule 08.05.2013
comment
@GertArnold: Со временем ваш совет прижился и служил лучше, чем выполнение акробатических трюков с классами сущностей.   -  person Raheel Khan    schedule 24.04.2016


Ответы (9)


Как насчет того, чтобы обернуть его в транзакцию?

    using(var scope = new TransactionScope(TransactionScopeOption.Required,
        new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted })){

        // Do something 
        context.SaveChanges();
        // Do something else
        context.SaveChanges();

        scope.Complete();
}
person Martin    schedule 08.05.2013
comment
Я должен добавить +1 к этому ответу просто потому, что было задано слишком много вопросов о последствиях многократного вызова context.SaveChanges в транзакции. Однако это не решает основного вопроса. - person Raheel Khan; 24.04.2016
comment
Потребовалось время, но это то, что мы в итоге использовали. Тем не менее, комментарий Герта Арнольда по этому вопросу следует отметить как наилучшую практику. - person Raheel Khan; 28.09.2016

public void RejectChanges()
    {
        foreach (var entry in ChangeTracker.Entries())
        {
            switch (entry.State)
            {
                case EntityState.Modified:
                case EntityState.Deleted:
                    entry.State = EntityState.Modified; //Revert changes made to deleted entity.
                    entry.State = EntityState.Unchanged;
                    break;
                case EntityState.Added:
                    entry.State = EntityState.Detached;
                    break;
            }
        }
    }

Обновление:

Некоторые пользователи предлагают добавить .ToList(), чтобы избежать исключения «коллекция была изменена». Но я считаю, что есть причина для этого исключения.

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

person Sergey Shuvalov    schedule 28.02.2014
comment
В случае Entity.Modified вам не нужно устанавливать для CurrentValues значение OriginalValues. Изменение состояния на Без изменений сделает это за вас ^.^! - person MaxVerro; 08.10.2015
comment
Смотрите мой ответ. Он добавляет поддержку изменения свойств навигации в этот отличный ответ. - person Jerther; 05.09.2017
comment
Для меня эта коллекция была изменена. Исключение. Изменено ChangeTracker.Entries() на ChangeTracker.Entries().ToList(), чтобы избежать исключения. - person T M; 30.08.2018
comment
context.TEntity.Local.Clear(); stackoverflow.com/questions/5466677/ - person RouR; 12.05.2019
comment
Не обязательно быть не потокобезопасным. Использование его исключительно синхронно в EFCore - person Captain Prinny; 15.10.2019

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

context.Entry(myEntity).CurrentValues.SetValues(context.Entry(myEntity).OriginalValues);
//you may also need to set back to unmodified -
//I'm unsure if EF will do this automatically
context.Entry(myEntity).State = EntityState.UnModified;

или альтернативно перезагрузить (но приводит к попаданию в БД)

context.Entry(myEntity).Reload();

person Community    schedule 08.05.2013
comment
Вам не нужно устанавливать для CurrentValues значение OriginalValues. Изменение сущности State на Unchanged сделает это за вас ^.^! - person MaxVerro; 08.10.2015
comment
Вызовет исключение, если myEntity имеет состояние Deleted. - person Jerther; 14.09.2016

Это основано на ответе хирурга Шувалова. Он добавляет поддержку изменения свойств навигации.

public void RejectChanges()
{
    RejectScalarChanges();
    RejectNavigationChanges();
}

private void RejectScalarChanges()
{
    foreach (var entry in ChangeTracker.Entries())
    {
        switch (entry.State)
        {
            case EntityState.Modified:
            case EntityState.Deleted:
                entry.State = EntityState.Modified; //Revert changes made to deleted entity.
                entry.State = EntityState.Unchanged;
                break;
            case EntityState.Added:
                entry.State = EntityState.Detached;
                break;
        }
    }
}

private void RejectNavigationChanges()
{
    var objectContext = ((IObjectContextAdapter)this).ObjectContext;
    var deletedRelationships = objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted).Where(e => e.IsRelationship && !this.RelationshipContainsKeyEntry(e));
    var addedRelationships = objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added).Where(e => e.IsRelationship);

    foreach (var relationship in addedRelationships)
        relationship.Delete();

    foreach (var relationship in deletedRelationships)
        relationship.ChangeState(EntityState.Unchanged);
}

private bool RelationshipContainsKeyEntry(System.Data.Entity.Core.Objects.ObjectStateEntry stateEntry)
{
    //prevent exception: "Cannot change state of a relationship if one of the ends of the relationship is a KeyEntry"
    //I haven't been able to find the conditions under which this happens, but it sometimes does.
    var objectContext = ((IObjectContextAdapter)this).ObjectContext;
    var keys = new[] { stateEntry.OriginalValues[0], stateEntry.OriginalValues[1] };
    return keys.Any(key => objectContext.ObjectStateManager.GetObjectStateEntry(key).Entity == null);
}
person Jerther    schedule 27.09.2016
comment
Исключение, связанное с KeyEntry, возникает, когда свойство Entity имеет значение null. Определяется так: get { return null == this._wrappedEntity; } из разобранного модуля. - person Marco Luzzara; 23.10.2018

Вы можете применить это:

context.Entry(TEntity).Reload();

Я пробую это, и это работает хорошо для меня.

Примечание. Этот метод (Reload) перезагружает сущность из базы данных, перезаписывая все значения свойств значениями из базы данных. Сущность будет находиться в состоянии Unchanged после вызова этого метода.

person Rayan Elmakki    schedule 16.11.2013
comment
Он также не будет обновлять другие подключенные сущности через внешние ключи. - person Dasith Wijes; 18.11.2016

Вы можете попробовать сделать это вручную, что-то вроде этого... не уверен, что это работает для вашего сценария, но вы можете попробовать:

public void UndoAll(DbContext context)
    {
        //detect all changes (probably not required if AutoDetectChanges is set to true)
        context.ChangeTracker.DetectChanges();

        //get all entries that are changed
        var entries = context.ChangeTracker.Entries().Where(e => e.State != EntityState.Unchanged).ToList();

        //somehow try to discard changes on every entry
        foreach (var dbEntityEntry in entries)
        {
            var entity = dbEntityEntry.Entity;

            if (entity == null) continue;

            if (dbEntityEntry.State == EntityState.Added)
            {
                //if entity is in Added state, remove it. (there will be problems with Set methods if entity is of proxy type, in that case you need entity base type
                var set = context.Set(entity.GeType());
                set.Remove(entity);
            }
            else if (dbEntityEntry.State == EntityState.Modified)
            {
                //entity is modified... you can set it to Unchanged or Reload it form Db??
                dbEntityEntry.Reload();
            }
            else if (dbEntityEntry.State == EntityState.Deleted)
                //entity is deleted... not sure what would be the right thing to do with it... set it to Modifed or Unchanged
                dbEntityEntry.State = EntityState.Modified;                
        }
    }
person Jurica Smircic    schedule 08.05.2013
comment
Это очень хорошо. Единственное, что я изменил, это то, что в случае удаления объекта, установленного для измененного объекта entityState, он не будет обрезан. Вместо этого dbEntityEntry.Reload(); обеспечит требуемый эффект (так же, как и в случае модифицированного объекта). - person Daniel; 04.05.2014
comment
@Daniel, это приведет к подключению к БД и может не работать, если у вас много удаленных объектов. Можете ли вы предложить альтернативу? - person Bamdad; 16.01.2021

Я столкнулся с проблемой с решением Jerther в ситуации, когда отношение, содержащее ключевую запись, было удалено, за исключением этого брошенный:

A relationship from the 'TableAValue_TableA' AssociationSet is in the 'Deleted' state. Given multiplicity constraints, a corresponding 'TableAValue_TableA_Source' must also in the 'Deleted' state.

Проблема, по-видимому, заключается в том, что RejectNavigationChanges() не может восстановить удаленную связь до ее предыдущего состояния, поскольку она содержит запись ключа, но связанные объекты уже восстановлены RejectScalarChanges().

Решение состоит в том, чтобы изменить способ, которым RejectScalarChanges() восстанавливает удаленные объекты, на использование entry.Reload().

Мое рабочее решение:

public void RejectChanges()
{
    RejectScalarChanges();
    RejectNavigationChanges();
}

private void RejectScalarChanges()
{
    var changedEntries = _dbContext.ChangeTracker.Entries()
        .Where(e => e.State != EntityState.Unchanged);

    foreach (var entry in changedEntries)
    {
        switch (entry.State)
        {
            case EntityState.Added:
                entry.State = EntityState.Detached;
                break;

            case EntityState.Modified:
                entry.State = EntityState.Unchanged; 
                break; 

            // Where a Key Entry has been deleted, reloading from the source is required to ensure that the entity's relationships are restored (undeleted).
            case EntityState.Deleted:
                entry.Reload();
                break;
        }
    }
}

private void RejectNavigationChanges()
{
    var objectContext = _dbContext.GetObjectContext();
    var addedRelationships = objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Added)
        .Where(e => e.IsRelationship);
    var deletedRelationships = objectContext.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted)
        .Where(e => e.IsRelationship && !RelationshipContainsKeyEntry(e));

    foreach (var relationship in addedRelationships)
        relationship.Delete();

    foreach (var relationship in deletedRelationships)
        relationship.ChangeState(EntityState.Unchanged);

    bool RelationshipContainsKeyEntry(ObjectStateEntry stateEntry)
    {
        var keys = new[] { stateEntry.OriginalValues[0], stateEntry.OriginalValues[1] };
        return keys.Any(key => objectContext.ObjectStateManager.GetObjectStateEntry(key).Entity == null);
    }
}

person Gary Pendlebury    schedule 23.11.2020

Я столкнулся с неприятным сюрпризом - вызов ChangeTracker.Entries() дает сбой, если вам нужно откатить изменения из-за исключения в DbContext, например.

System.InvalidOperationException: 
'The property 'Id' on entity type 'TestEntity' is part of a key and so cannot be modified or marked as modified. 
To change the principal of an existing entity with an identifying foreign key first delete the dependent and invoke 'SaveChanges' then associate the dependent with the new principal.'

вот я и придумал хакнутую версию ручного отката

    public async Task RollbackChanges()
    {
        var oldBehavoir = ChangeTracker.QueryTrackingBehavior;
        var oldAutoDetect = ChangeTracker.AutoDetectChangesEnabled;

        // this is the key - disable change tracking logic so EF does not check that there were exception in on of tracked entities
        ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
        ChangeTracker.AutoDetectChangesEnabled = false;

        var entries = ChangeTracker.Entries().ToList();

        foreach (var entry in entries)
        {
            switch (entry.State)
            {
                case EntityState.Modified:
                    await entry.ReloadAsync();
                    break;
                case EntityState.Deleted:
                    entry.State = EntityState.Modified; //Revert changes made to deleted entity.
                    entry.State = EntityState.Unchanged;
                    break;
                case EntityState.Added:
                    entry.State = EntityState.Detached;
                    break;
            }
        }

        ChangeTracker.QueryTrackingBehavior = oldBehavoir;
        ChangeTracker.AutoDetectChangesEnabled = oldAutoDetect;
    }
person Evgeny    schedule 04.03.2018
comment
Я не могу проверить в данный момент, но, возможно, вы могли бы попробовать позвонить ChangeTracker.Entries() перед отключением отслеживания. - person Raheel Khan; 05.03.2018
comment
Вылетает... Приходится все это выбрасывать, в других сценариях не работает - person Evgeny; 09.03.2018
comment
Со всеми улучшениями, которые мы вносим в исходный код, я думаю, что стоит создать новый проект на github вместе с пакетом NuGet. Просто идея, поскольку я больше не работаю с EF. - person Jerther; 22.01.2019

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

DbContextObject.ChangeTracker.Clear()

Пожалуйста, обратитесь к ссылке ниже для справки.

https://docs.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.changetracking.changetracker.clear?view=efcore-5.0

person Nitin Jain    schedule 15.06.2021
comment
Нет, это только очищает трекер изменений. Не изменения в сущностях. Кроме того, теперь все объекты должны быть повторно прикреплены, чтобы сохранить любые новые изменения. Это не решает проблему ОП. - person Gert Arnold; 15.06.2021
comment
@GertArnold Вот пример, когда это может быть полезно: скажем, вы сохраняете некоторые записи в базе данных, и это не удалось из-за определенной ошибки. Вы регистрируете свои ошибки в БД в промежуточном программном обеспечении, поэтому вам нужно игнорировать любой возникающий сбой и вставлять запись журнала ошибок в таблицу журнала, используя тот же контекст БД, чтобы не создавать новый экземпляр. Таким образом, вы очищаете все подобные изменения, не заботясь о данных, поскольку вы будете сохранять запись журнала ошибок и возвращать ответ об ошибке. - person Ziad Akiki; 24.07.2021
comment
Все хорошо и хорошо, но это не отменяет никаких изменений, внесенных по желанию OP. Вы отвечаете исходя из собственного настроения, а не из ОП. - person Gert Arnold; 24.07.2021