簡體   English   中英

DbContext 丟棄更改而不處理

[英]DbContext discard changes without disposing

我有一個桌面客戶端應用程序,它使用模式窗口來設置分層對象的屬性。 由於這是一個客戶端應用程序並且對 DbContext 的訪問不是線程化的,因此我在主窗體上使用了一個長時間運行的上下文,該上下文被傳遞給模態子級。

這些模式窗口使用 PropertyGrid 來顯示實體屬性,並且還有取消按鈕。 如果修改了任何數據並按下了取消按鈕,則更改會反映在父窗體中(我無法在其中處理DbContext object )。

如果未DbContext.SaveChanges()方法,有沒有辦法放棄所做的任何更改?

更新:實體框架版本 4.4。

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

把它包裝在一個事務中怎么樣?

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

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

        scope.Complete();
}

這是基於 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);
}

你可以應用這個:

context.Entry(TEntity).Reload();

我嘗試了它,它對我來說效果很好。

注意:此方法 ( Reload ) 從數據庫重新加載實體,用數據庫中的值覆蓋任何屬性值。 調用此方法后,實體將處於 Unchanged 狀態。

您可以嘗試手動執行此操作,就像這樣.. 不確定這是否適用於您的場景,但您可以嘗試一下:

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

我在Jerther 的解決方案中遇到了一個問題,在包含 Key Entry 的關系已被刪除的情況下,拋出此異常:

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()無法將已刪除的關系恢復到以前的狀態,因為它包含一個 Key Entry,但是關聯的對象已經被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);
    }
}

我遇到了令人討厭的驚喜 - 如果由於 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;
    }

如果我們想放棄所有更改而不管任何類型的更改,在實體框架核心中,我們可以一步完成。

DbContextObject.ChangeTracker.Clear()

請參閱以下鏈接以供參考。

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM