DbContext丢弃更改而不进行处理

时间:2013-05-08 09:27:07

标签: c# .net entity-framework dbcontext undo

我有一个桌面客户端应用程序,它使用模态窗口来设置分层对象的属性。由于这是一个客户端应用程序,并且对DbContext的访问没有线程化,因此我在主Form上使用了一个长时间运行的上下文,它被传递给模态子项。

这些模态窗口使用PropertyGrid显示实体属性,还有取消按钮。如果修改了任何数据并按下了取消按钮,则更改将反映在父表单中(我无法处理DbContext object)。

如果没有调用DbContext.SaveChanges()方法,有没有办法放弃所做的更改?

更新:实体框架版本4.4。

9 个答案:

答案 0 :(得分:118)

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()以避免'收集被修改'例外。 但我相信这个例外是有原因的。

你如何得到这个例外?可能,您正在以非线程安全的方式使用上下文。

答案 1 :(得分:16)

在取消对单个实体的属性所做的更改的简单情况下,您可以将当前值设置为原始值。

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;

或者重新加载(但会导致db命中)

context.Entry(myEntity).Reload();

答案 2 :(得分:11)

如何在交易中包装它?

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

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

        scope.Complete();
}

答案 3 :(得分:5)

这是基于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);
}

答案 4 :(得分:4)

您可以尝试手动执行此类操作..不确定这适用于您的方案,但您可以尝试一下:

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

答案 5 :(得分:2)

您可以申请:

context.Entry(TEntity).Reload();

我尝试了它并且对我来说效果很好。

注意:此方法(重新加载)从数据库重新加载实体,使用数据库中的值覆盖任何属性值。调用此方法后,实体将处于Unchanged状态。

答案 6 :(得分:0)

我遇到了令人讨厌的惊喜 - 如果你需要回滚由于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;
    }

答案 7 :(得分:0)

我遇到了Jerther's solution的问题,其中包含键项的关系已被删除,但抛出了此异常:

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

答案 8 :(得分:-1)

如果我们想放弃所有更改而不管任何类型的更改,在实体框架核心中,我们可以一步完成。

DbContextObject.ChangeTracker.Clear()

请参考以下链接。

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