DbContext отменить изменения без утилизации
У меня есть настольное клиентское приложение, которое использует модальные окна для установки свойств для иерархических объектов. Поскольку это клиентское приложение и доступ к DbContext не является многопоточным, я использую длительный контекст в главной форме, который передается модальным дочерним элементам.
Эти модальные окна используют PropertyGrid для отображения свойств объекта, а также имеют кнопки отмены. Если какие-либо данные изменены и кнопка отмены нажата, изменения отражаются в родительской форме (где я не могу DbContext object
).
Есть ли способ отменить любые изменения, если DbContext.SaveChanges()
метод не был вызван?
ОБНОВЛЕНИЕ: Entity Framework версии 4.4.
9 ответов
Как насчет упаковки в транзакции?
using(var scope = new TransactionScope(TransactionScopeOption.Required,
new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted })){
// Do something
context.SaveChanges();
// Do something else
context.SaveChanges();
scope.Complete();
}
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;
}
}
}
В простом случае отмены изменений, внесенных в свойства отдельной сущности, вы можете установить текущие значения в исходные значения.
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();
Это основано на ответе Сергея Шувалова. Добавлена поддержка изменения свойств навигации.
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);
}
Вы пытаетесь сделать это вручную, что-то вроде этого... не уверен, что это работает для вашего сценария, но вы можете попробовать:
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;
}
}
Вы можете применить это:
context.Entry(TEntity).Reload();
Я стараюсь, и это хорошо работает для меня.
Примечание. Этот метод (перезагрузка) перезагружает сущность из базы данных, перезаписывая любые значения свойств значениями из базы данных. После вызова этого метода объект будет в неизмененном состоянии.
Если мы хотим отменить все изменения независимо от любого типа изменений, в ядре Entity Framework мы можем сделать это за один шаг.
DbContextObject.ChangeTracker.Clear()
Пожалуйста, перейдите по ссылке ниже для справки.
Я столкнулся с проблемой с решением Джертера в ситуации, когда связь, содержащая ключевую запись, была удалена, и было выбрано следующее исключение:
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);
}
}
Я столкнулся с неприятным сюрпризом - вызов 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;
}