【问题标题】:DbContext discard changes without disposingDbContext 丢弃更改而不释放
【发布时间】:2013-05-02 11:18:51
【问题描述】:

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

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

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

更新:实体框架版本 4.4。

【问题讨论】:

  • 应用程序不会在其整个生命周期中保留 DbContext 对象。编辑分层对象也是需要编辑子级的工作单元。就我而言,我被模态窗口和连接/附加实体卡住了。
  • 在模态窗口中使用 DTO(或已编辑对象的克隆)。取消编辑后,只需丢弃 DTO,原始对象不会发生任何事情。当您要保存时,首先将 DTO 值复制到原始对象并保存更改。
  • @GertArnold:随着时间的推移,你的建议比在实体类上执行杂技更有效。

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


【解决方案1】:
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() 以避免'collection was modified'异常。 但我相信这个例外是有原因的。

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

【讨论】:

  • Entity.Modified 情况下,您不需要将 CurrentValues 设置为 OriginalValues。将状态更改为 Unchanged 将为您完成 ^.^!
  • 看我的回答。它为这个出色的答案添加了对导航属性更改的支持。
  • 对我来说,这是在抛出“collection was modified ..”异常。将 ChangeTracker.Entries() 更改为 ChangeTracker.Entries().ToList() 以避免异常。
  • context.TEntity.Local.Clear(); stackoverflow.com/questions/5466677/…
  • 不必采用非线程安全的方式。在 EFCore 中完全同步使用它
【解决方案2】:

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

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

【讨论】:

  • 您不需要将 CurrentValues 设置为 OriginalValues。将实体 State 更改为 Unchanged 将为您完成 ^.^!
  • 如果 myEntity 的状态设置为 Deleted,将抛出异常。
【解决方案3】:

将其包装在事务中怎么样?

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

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

        scope.Complete();
}

【讨论】:

  • 我必须 +1 这个答案仅仅是因为关于在事务中多次调用 context.SaveChanges 的影响已经被问到太多问题。然而,这并没有解决核心问题。
  • 花了一段时间,但这是我们最终使用的。 @Gert Arnold 对这个问题的评论应该被认为是最佳实践。
【解决方案4】:

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

【讨论】:

  • Entity属性为null时,会抛出与KeyEntry相关的异常。它的定义是这样的:来自反汇编模块的get { return null == this._wrappedEntity; }
【解决方案5】:

您尝试手动执行此操作,类似这样.. 不确定这是否适用于您的场景,但您可以尝试一下:

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

【讨论】:

  • 这很好。我唯一改变的是,在删除实体的情况下,将 entityState 设置为 modified 不会删除它。而是 dbEntityEntry.Reload();将提供所需的效果(就像在修改实体的情况下一样)。
  • @Daniel 好吧,它会导致与数据库的连接,并且当您有很多已删除的实体时可能无法执行。你能推荐一个替代方案吗?
【解决方案6】:

你可以应用这个:

context.Entry(TEntity).Reload();

我试了一下,效果很好。

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

【讨论】:

  • 也不会通过外键刷新其他连接实体。
【解决方案7】:

我遇到了Jerther's solution 的问题,在包含 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);
    }
}

【讨论】:

    【解决方案8】:

    我遇到了令人讨厌的意外 - 如果您由于 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;
        }
    

    【讨论】:

    • 我目前无法测试,但也许您可以尝试在禁用跟踪之前致电ChangeTracker.Entries()
    • 它崩溃了......我必须废弃这一切,在其他场景中不起作用
    • 随着我们对原始代码的所有改进,我认为在 github 上创建一个新项目以及一个 NuGet 包是值得的。只是一个想法,因为我不再使用 EF 了。
    【解决方案9】:

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

    DbContextObject.ChangeTracker.Clear()
    

    请参考以下链接。

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

    【讨论】:

    • 不,这只会清除更改跟踪器。不是实体的变化。此外,现在必须重新附加所有实体以保存任何新更改。这并不能解决 OP 的问题。
    • @GertArnold 这是一个可能有用的示例:假设您正在将一些记录保存到数据库中,但由于某个错误而失败。您正在将错误记录到中间件中的数据库中,因此您需要忽略发生的任何故障并使用相同的数据库上下文在日志表中插入错误日志条目,以免创建新实例。因此,您清除所有此类更改而不关心数据,因为您将保存错误日志条目并返回错误响应。
    • 一切都很好,但这并不像 OP 所希望的那样“丢弃所做的任何更改”。你是从你自己的思维框架中回答的,而不是 OP 的。
    猜你喜欢
    • 2014-03-31
    • 1970-01-01
    • 2021-09-20
    • 2020-06-01
    • 2012-05-25
    • 1970-01-01
    • 2017-06-26
    • 2015-05-23
    • 1970-01-01
    相关资源
    最近更新 更多