69

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

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

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

更新:实体框架版本 4.4。

4

9 回答 9

161
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()以避免“集合被修改”异常。但我相信这个例外是有原因的。

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

于 2014-02-28T14:35:57.220 回答
18

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

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

于 2013-05-08T22:38:31.913 回答
13

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

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

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

        scope.Complete();
}
于 2013-05-08T09:37:57.610 回答
8

这是基于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);
}
于 2016-09-27T15:07:42.210 回答
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;                
        }
    }
于 2013-05-08T10:21:33.093 回答
5

你可以应用这个:

context.Entry(TEntity).Reload();

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

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

于 2013-11-16T01:55:37.957 回答
1

我在使用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);
    }
}

于 2020-11-23T15:51:15.830 回答
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;
    }
于 2018-03-04T21:57:12.687 回答
0

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

DbContextObject.ChangeTracker.Clear()

请参阅下面的链接以供参考。

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

于 2021-06-15T07:23:28.540 回答