モーダルウィンドウを使用して階層オブジェクトのプロパティを設定するデスクトップクライアントアプリケーションがあります。これはクライアントアプリケーションであり、DbContextへのアクセスはスレッド化されないため、モーダルの子に渡されるメインフォームで長時間実行されるコンテキストを使用します。
これらのモーダルウィンドウは、PropertyGridを使用してエンティティプロパティを表示し、キャンセルボタンも備えています。データが変更されてキャンセルボタンが押されると、変更は親フォームに反映されます(ここでは_DbContext object
_を破棄できません)。
DbContext.SaveChanges()
メソッドが呼び出されていない場合に行われた変更を破棄する方法はありますか?
PDATE: Entity Frameworkバージョン4.4。
トランザクションでラップするのはどうですか?
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;
}
}
}
_
更新:
一部のユーザーは、「コレクションが変更されました」例外を回避するために.ToList()
を追加することを提案しています。しかし、この例外には理由があると思います。
この例外はどのように発生しますか?おそらく、スレッドセーフでない方法でコンテキストを使用しています。
単一のエンティティのプロパティに加えられた変更をキャンセルする単純なケースでは、現在の値を元の値に設定できます。
_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();
これは、Surgey Shuvalovの回答に基づいています。ナビゲーションプロパティの変更のサポートが追加されます。
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();
私はそれとその仕事をうまくやっています。
注:このメソッド(Reload)は、データベースからエンティティを再ロードし、プロパティ値をデータベースの値で上書きします。このメソッドを呼び出した後、エンティティはUnchanged状態になります。
私は厄介な驚きに出会いました-DbContextの例外のために変更をロールバックする必要がある場合、ChangeTracker.Entries()への呼び出しがクラッシュします.
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;
}