65.9K
CodeProject 正在变化。 阅读更多。
Home

为 Entity Framework 的 DbContext 实现撤销/重做功能

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (10投票s)

2012年10月10日

CPOL

6分钟阅读

viewsIcon

59008

downloadIcon

1929

Entity Framework DbContext的撤销重做

引言

我之前对 DbContext 的变更跟踪和属性值感到好奇,在研究时,我决定编写一个快照管理器。它通过存储条目来为我的每个变更创建快照,并且这个管理器还允许我对 DbContext 中的变更进行后退和前进操作。撤销和重做功能让你可以轻松纠正错误,或根据某些场景进行操作,同时也让你能够自由地尝试不同的路由和数据映射决策。撤销会逆转你执行的最后一个操作,而重做则会撤销上一次的撤销操作。

背景

当变更跟踪功能启用时,DbContext 会检查所有类型的所有实体,并验证它们的数据是否有任何变化。自动变更跟踪功能默认是启用的。禁用它将不会为实体中的每个变更触发 DbContext 的更新。实际上,它维护了实体的状态。当调用 SaveChanges() 方法时,它会使用这些状态来确定需要推送到数据库的变更。禁用变更跟踪仍然允许我们检查实体的旧值和当前值,但它会将状态保持为 UnChanged(未更改),直到检测到变更为止。我们需要在 DbContext 上手动调用 DetectChanges() 来更新它们。在某些情况下,Entity Framework API 也会隐式调用它。

DbContext.DetectChanges() 会在以下情况下被隐式调用:

  • DbSet 上的 AddAttachFindLocalRemove 成员
  • DbContext 上的 GetValidationErrorsEntrySaveChanges 成员
  • DbChangeTracker 上的 Entries 方法

DetectChanges 是作为 SaveChanges 实现的一部分被调用的。这意味着,如果你在上下文中重写了 SaveChanges,那么在你的 SaveChanges 方法被调用之前,DetectChanges 将不会被调用。这有时会让人感到困惑,尤其是在检查一个实体是否被修改时,因为在调用 DetectChanges 之前,它的状态可能不会被设置为“已修改”(Modified)。相比于 ObjectContext.SaveChanges,这种情况在 DbContext.SaveChanges 中发生的频率要低得多,因为用于访问实体状态的 Entry 和 Entries 方法会自动调用 DetectChanges。与 SaveChanges 不同,ValidateEntity 是在 DetectChanges 被调用之后才调用的。这是因为验证需要针对将要保存的内容进行,而这些内容只有在调用 DetectChanges 之后才能确定。

实现

那么,让我们回到创建变更快照的主题上。由于 DbContext 仍然没有提供任何关于 DbContext.DetectChanges() 执行的事件,我决定在我的 Repository 的 CUD(创建更新删除)操作中保存我的快照,并且允许用户在需要时显式调用它。

public void Add(T entity)
{
    this.Context.GetEntitySet<T>().Add(entity);
    this.Context.SnapshotManager.TakeSnapshot();
}

public void Update(T entity)
{
    this.Context.ChangeState(entity, System.Data.EntityState.Modified);
    this.Context.SnapshotManager.TakeSnapshot();
}

public void Remove(T entity)
{
    this.Context.ChangeState(entity, System.Data.EntityState.Deleted);
    this.Context.SnapshotManager.TakeSnapshot();
}

并且也允许用户访问快照管理器。

现在,你可能会问我 ISnapshotManager 的定义是什么。以下是快照管理器将为我们提供的功能:

public interface ISnapshotManager
{
    void TakeSnapshot();
    void UnDo();
    void Redo();
    bool CanUndo();
    bool CanRedo();
}

让我们逐一来看每个任务的实现。

首先,我们来看看创建快照的任务。在此之前,我想提醒你关于 DbContext 的变更跟踪选项,如果“DetectChanges()”已被执行,我们将从中获取变更条目。

如你所知,通过为 DbContext 分别设置 AutoDetectChangesEnabledtrue / false,可以启用/禁用变更跟踪。在 DbContext API 中,Entity Framework 会为每个被跟踪实体的每个属性记录两个值。当前值(current value),顾名思义,是实体中属性的当前值。原始值(original value)是当实体从数据库查询出来或附加到上下文时该属性所具有的值。一旦我们获得了变更条目,我们就可以访问 OriginalValuesCurrentValues 属性。它们都是 DbPropertyValues 类型。它是底层实体或复杂对象所有属性的集合。

public void TakeSnapshot()
{
    _redoDoList.Clear();
    if(!this.Configuration.AutoDetectChangesEnabled)
        this.ChangeTracker.DetectChanges();
    var entries = this.ChangeTracker.Entries().Where( e => e.State == EntityState.Added || 
        e.State == EntityState.Modified || e.State == EntityState.Deleted );
    if(null != entries)
    {
        var entrySnapList = new List<SnapData>();
        foreach (var entry in entries)
        {                    
            if (entry.Entity != null 
                && !_unDoList.Any(v => v.Any(s => s.Entity.Equals(entry.Entity))) )
            {
                entrySnapList.Add(new SnapData()
                {
                    OrginalValue = (entry.State == EntityState.Deleted || 
                                    entry.State == EntityState.Modified) ?
                    (entry.OriginalValues != null) ? entry.OriginalValues.ToObject() : 
                                                     entry.GetDatabaseValues()
                    : null,
                    Value = (entry.State == EntityState.Added || 
                      entry.State == EntityState.Modified) ? 
                                     entry.CurrentValues.ToObject() : null,
                    State = entry.State,
                    Entity = entry.Entity
                });
            }
        }
        if (entrySnapList.Count > 0)
            _unDoList.Push(entrySnapList.AsEnumerable());
    }
} 

在这里,我保留了两个堆栈列表(Stack List),用于将变更条目保存到撤销(UnDo)列表以便后退,以及重做(ReDo)列表以便前进。一旦一个条目被列入撤销列表,直到它被弹出之前,它不会被重复添加。只有已添加、已修改或已删除的条目才被考虑拥有撤销-重做功能。除了条目的状态,我在这里还保留了属性。一个已添加的条目只有当前属性,而已删除的条目只有原始属性,而已修改的条目则两者都有。我使用 ToObject() 方法将这些属性值的克隆保存在一个名为 SnapData 的载体中。

class SnapData
{
    public EntityState State;
    public object Value;
    public object OrginalValue;
    public object Entity;
}

其次,在 Undo 方法中,我弹出条目并将其状态设为 Unchanged(未更改),以便 DbContext 在向数据库提交更改时会忽略它们。

public void UnDo()
{
    if (CanUndo())
    {
        bool previousContiguration = this.Configuration.AutoDetectChangesEnabled;
        this.Configuration.AutoDetectChangesEnabled = false;
        var entries = _unDoList.Pop();
        {
            this._redoDoList.Push(entries);
            foreach (var snap in entries)
            {
                var currentEntry = this.Entry(snap.Entity);
                if (snap.State == EntityState.Modified)
                {
                    currentEntry.CurrentValues.SetValues(snap.OrginalValue);
                    var dbValue = currentEntry.GetDatabaseValues();
                    currentEntry.OriginalValues.SetValues(dbValue);
                }
                else if (snap.State == EntityState.Deleted)
                {
                    var dbValue = currentEntry.GetDatabaseValues();
                    currentEntry.OriginalValues.SetValues(dbValue);
                }
                currentEntry.State = EntityState.Unchanged;
            }
        }
        this.Configuration.AutoDetectChangesEnabled = previousContiguration;
    }
}

在这里,在操作条目之前,变更跟踪已被关闭。如果变更跟踪是开启的,DbContext 在改变状态(如:未更改、已分离)时会移除外键关系。为了保持对象原样,我决定先将其关闭,完成我的工作后再恢复到之前的配置。

一个实体的所有属性值都可以读入一个 DbPropertyValues 对象中。然后,DbPropertyValues 就像一个类似字典的对象,允许读取和设置属性值。DbPropertyValues 对象中的值可以从另一个 DbPropertyValues 对象中的值设置,也可以从其他某个对象中的值设置。对于已修改和已删除的条目,我用数据库的值设置了原始值,并将之前的原始值设置为已修改条目的当前值。获取数据库的值很有用,特别是当数据库中的值可能在实体被查询后发生了变化,例如当另一用户对数据库进行了并发编辑时。(有关处理乐观并发的更多详情,请参见第 9 部分。)

第三,在重做(Redo)操作中,我将它们的 State(状态)和值一起恢复。已添加的条目不需要这样做,而已删除的条目应该总是与它存储在数据库中的值保持一致。但已修改的条目需要恢复更改,我通过我存储的值来做到这一点。

public void Redo()
{
    if (CanRedo())
    {
        bool previousContiguration = this.Configuration.AutoDetectChangesEnabled;
        this.Configuration.AutoDetectChangesEnabled = false;
        var entries = _redoDoList.Pop();
        if (null != entries && entries.Count() > 0)
        {
            foreach (var snap in entries)
            {
                var currentEntry = this.Entry(snap.Entity);

                if (snap.State == EntityState.Modified)
                {
                    currentEntry.CurrentValues.SetValues(snap.Value);
                    currentEntry.OriginalValues.SetValues(snap.OrginalValue);
                }
                else if (snap.State == EntityState.Deleted)
                {
                    var dbValue = currentEntry.GetDatabaseValues();
                    currentEntry.OriginalValues.SetValues(dbValue);
                }
                currentEntry.State = snap.State;
            }
        }
        this.Configuration.AutoDetectChangesEnabled = previousContiguration;
    }
}

现在,一旦你的更改被保存或提交到数据库,你需要清空你的撤销和重做列表,因为在那之后你的条目将被清除。

public void ClearSnapshots()
{
    _redoDoList.Clear();
    _unDoList.Clear();
}  

现在让我们来使用这个实现。

using (var db = _unitOfWork.Value)
{
    db.EnableAuditLog = false; 
    using (var transaction = db.BeginTransaction())
    {
        try
        {
            IRepository<BlogPost> blogRepository = db.GetRepository<BlogPost>();
            blogRepository.Add(post);
            
            var blog = blogRepository.FindSingle(b => !b.Id.Equals(post.Id)); ;
            blog.Title = "I am changed again!!";
            blogRepository.Update(blog);
            db.UnDo();
            db.UnDo();

            int i = db.Commit();
            transaction.Commit();
            return (i > 0);
        }
        catch (Exception)
        {
            transaction.Rollback();
            throw;
        }
    }
} 

在这里,我做了两次更改,然后请求撤销两次。结果会是什么?是的,结果将是零,db.Commit() 将通过受影响的行数返回这个结果给你。你可以在这里使用重做(ReDo)来前进你的更改。UnitOfWork 包含一个 IContext,它为我提供了我之前定义的快照管理器。通过使用那个管理器,它通常在我的业务层中提供撤销/重做功能。

看点

变更跟踪和 DetectChanges 方法是技术栈中会带来性能开销的一部分。因此,了解实际发生了什么会很有用,这样如果默认行为不适合你的应用程序,你可以做出明智的决策。如果你的上下文没有跟踪大量的实体,你几乎永远不需要关闭自动 DetectChanges,否则我建议你关闭它,并在必要时调用 DetectChanges

参考文献

历史

  • 2012年10月10日:初始版本
© . All rights reserved.