Click here to Skip to main content
Click here to Skip to main content

Implementing Undo/Redo feature for DbContext of Entity Framework

By , 11 Oct 2012
Rate this:
Please Sign up or sign in to vote.

Introduction

I was curious about change tracking and property values of DbContext and while playing with that I decided to write snapshot manager which would take snapshot of my each changes by storing entries And this manager also allow me to backward and forward my changes with DbContext. The Undo and Redo features allow you to easily correct mistakes or based on some scenario, as well as free you to experiment with different routing and data mapping decisions. Undo reverses the last action you performed, and Redo undoes the last Undo action.

Background

DbContext will check all the entities of all types when change tracking is enabled and also verify if they have any changes in their data. Automatic Change tracking is enabled by default. Disabling it would not trigger the DbContext update for each change in the entity. Actually it maintains the state of entities. It uses this to determine the changes needed to be pushed to database when SaveChanges() is called. Disabling change tracking would still allow us to check the old and current values of an entity but it would keep the state as UnChanged until the changes are detected. We need to manually call DetectChanges() on DbContext to update them. There are instances in which it is called implicitly by Entity Framework API.

DbContext.DetectChanges() is implicitly called as follows:

  • The Add, Attach, Find, Local or Remove members on DbSet
  • The GetValidationErrors, Entry, or SaveChanges members on DbContext
  • The Entries method on DbChangeTracker

DetectChanges is called as part of the implementation of the SaveChanges. This means that if you override SaveChanges in your context, then DetectChanges will not have been called before your SaveChanges method is called. This can sometimes catch people out, especially when checking if an entity has been modified or not since its state may not be set to Modified until DetectChanges is called. This happens a lot less with the DbContext.SaveChanges than it used to with ObjectContext.SaveChanges because the Entry and Entries methods that are used to access entity state automatically call DetectChanges. Unlike SaveChanges, ValidateEntity is called after DetectChanges has been called. This is because validation needs to be done on what is going to be saved, and this is only known after DetectChanges has been called.

Implementation

So let’s come to point of creating our snapshots of changes. Since DbContext still doesn’t give any event of DbContext.DetectChanges() execution, I have decided to keep my snapshot in my Repository’s CUD( Create , Update , Delete) operations and also allow user to call it explicitly whenever it is needed-

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

And also allow user to access Snapshot Manager-

So now, you can ask me the definition of ISnapshotManager. Here is a feature that Snapshot Manager are going to provide us–

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

Lets come to the implementation of each task one by one.

Firstly, lets take a look into the task of taking snapshots. Before that, I would like to remind you the change tracking option of DbContext, where we will get the entries of change if ‘DetectChanges()’ has been executed –

As you know, Change tracking can be enabled/disabled by setting AutoDetectChangesEnabled to true / false respectively for DbContext. In DbContext API. The Entity Framework keeps track of two values for each property of a tracked entity. The current value is, as the name indicates, the current value of the property in the entity. The original value is the value that the property had when the entity was queried from the database or attached to the context. Once we get the change entries, we can access OriginalValues and CurrentValues properties. Both of them are of type DbPropertyValues. It is a collection of all the properties of an underlying entity or a complex object.

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

Here I have kept two Stack Lists here to keep the changes entries into UnDo list to go backward and ReDo list go forward. Once an entry has been listed into undo list, it will not consider again adding until it has been popped. Only added, modified or deleted entries have been considered to have undo-redo facility. Beside the state of entries I am also keeping here the properties. An added entry only has the current properties and deleted entries has only original properties and modified has the both. I am keeping the clone of those property-values using ToObject() method into the carrier named - SnapData.

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

Secondly, on Undo method, I pop the entries and make the state into Unchanged so that DbContext will ignore them while submitting the changes into database.

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

Here, change tracking has been switched off before manipulating the entries. If Change tracking has been turn on, DbContext will set remove foreign key relationship on changing the state like –unchanged, detached. To keep the object as it was I have decided to make it turn it off and back to previous configuration after finishing my job.

Values for all properties of an entity can be read into a DbPropertyValues object. DbPropertyValues then acts as a dictionary-like object to allow property values to be read and set. The values in a DbPropertyValues object can be set from values in another DbPropertyValues object or from values in some other object. For modified and deleted entries, I have set Original values with database values and previous original value has been set as current values of modified entries. Getting the database values is useful when the values in the database may have changed since the entity was queried such as when a concurrent edit to the database has been made by another user. (See Part 9 for more details on dealing with optimistic concurrency.

Thirdly, In Redo I give their State back with their values. Added entries doesn't need that so and deleted entries is always should be with its values stored in database. But Modified entries need changes back and I do that so with my storing values.

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

Now, you need to clear you Undo and Redo list once it has been saved or submitted to database since your entries going to be cleared after that.

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

Let's use this implementation now :

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

Here I do two changes and ask for undo twice, So what will be the result? Yes, It will be Zero and db.Commit() will return you so by the affecting rows count. You can Use ReDo to forward you changes here. UnitOfWork has IContext which provide me Snapshot Manager as I defined earlier. Using that manager it is generally  providing the UnDo/Redo feature in my business layer.    

Point of Interest

Change tracking and the DetectChanges method are a part of the stack where there is a penalty of performance. For this reason it can be useful to have an idea of what is actually going on such you can make an informed decision on what to do if the default behaviour is not right for your application. If your context is not tracking a large number of entities you pretty much never need to switch off automatic DetectChanges otherwise I will suggest you turned it off and call DetectChanges where it seems necessary.

References

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Morshed Anwar
Team Leader Adaptive Enterprise Limited (www.ael-bd.com)
Bangladesh Bangladesh
No Biography provided

Comments and Discussions

 
QuestionThis is great [modified] PinmemberJohann Strydom13-Mar-14 0:56 
AnswerRe: This is great PinpremiumMorshed Anwar13-Mar-14 23:27 
GeneralMy vote of 5 PinmemberAmir Mehrabi-Jorshari10-Jun-13 10:09 
GeneralRe: My vote of 5 PinmemberMorshed Anwar11-Jun-13 0:11 
GeneralMy vote of 5 PinmemberАslam Iqbal23-Apr-13 23:25 
GeneralRe: My vote of 5 PinmemberMorshed Anwar11-Jun-13 0:12 
GeneralRe: My vote of 5 PinmemberАslam Iqbal11-Jun-13 0:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140415.2 | Last Updated 11 Oct 2012
Article Copyright 2012 by Morshed Anwar
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid