Click here to Skip to main content
15,884,099 members
Articles / Programming Languages / C#

Implementing Undo/Redo feature for DbContext of Entity Framework

Rate me:
Please Sign up or sign in to vote.
4.95/5 (10 votes)
11 Oct 2012CPOL6 min read 57.4K   1.9K   48   7
Undo Redo with DbContext of Entity Framework
This post discusses a Snapshot manager that I wrote which takes a snapshot of each of my changes by storing entries, and also allows me to backward and forward my changes with DbContext.

Introduction

I was curious about change tracking and property values of DbContext and while playing with that, I decided to write a snapshot manager which would take a snapshot of each of my changes by storing entries, and this manager also allows 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 the 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:

C#
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:

Image 1

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

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

Let's come to the implementation of each task one by one.

Firstly, let's take a look into the task of taking snapshots. Before that, I would like to remind you about 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.

C#
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 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 adding again until it has been popped. Only added, modified or deleted entries have been considered to have undo-redo facility. Beside the state of entries, here I am also keeping the properties. An added entry only has the current properties and deleted entries has only original properties and modified has both. I am keeping the clone of those property-values using ToObject() method into the carrier named - SnapData.

C#
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.

C#
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 turned 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 the 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 don't need that so deleted entries should always be with its values stored in database. But Modified entries need changes back and I do that with my storing values.

C#
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 your Undo and Redo list once it has been saved or submitted to database since your entries are going to be cleared after that.

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

Let's use this implementation now:

C#
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 make 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 so that 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 turn it off and call DetectChanges where it seems necessary.

References

History

  • 10th October, 2012: Initial version

License

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


Written By
Team Leader PracticePRO Software Systems Inc
United States United States
In my childhood, my uncle has shown me how to see the cloud in a close look and I understand that one can draw some elements of the Earth in the sky-canvas if he/she wants to. After that the cloud becomes closer to me and It teaches me one thing that, a deeper-look to something will give you some clues to draw your imagination. You can able to see that one which you have build-up in your mind.

Years past, I have started my career as a software engineer and has been looking for passion in my coding and development which I should be to enjoy my profession and has started asking myself- 'am I doing any engineering here?!' Is my code becoming that thing which I have designed in my mind? So to find that answer I have tried that old solution here... I have decided to come closer to my code and start analyzing them. And it is really working for me and at least it gives me the confidence that I can build something that I really want to. I can draw my thinking there through my code and can build-up my vision that I have designed in my mind. It also helps me to think out of the box, solve each problems by making blocks and make me careful on each steps.

• Morshed's Technical Blog site: http://morshedanwar.wordpress.com/

• Morshed's Technical articles those are published in Codeproject site: http://www.codeproject.com/script/Articles/MemberArticles.aspx?amid=2992452

• Morshed's Linkedin profile: http://www.linkedin.com/in/morshedanwar

• Morshed's Facebook Profile : http://www.facebook.com/morshed.pulok

Beside all these I like to do - photography and music. Here is my Flickr photos : http://www.flickr.com/photos/morshed_anwar/

Comments and Discussions

 
QuestionThis is great Pin
Johann Strydom13-Mar-14 0:56
Johann Strydom13-Mar-14 0:56 
AnswerRe: This is great Pin
Morshed Anwar13-Mar-14 23:27
professionalMorshed Anwar13-Mar-14 23:27 
GeneralMy vote of 5 Pin
Amir Mehrabi-Jorshari10-Jun-13 10:09
Amir Mehrabi-Jorshari10-Jun-13 10:09 
GeneralRe: My vote of 5 Pin
Morshed Anwar11-Jun-13 0:11
professionalMorshed Anwar11-Jun-13 0:11 
GeneralMy vote of 5 Pin
Аslam Iqbal23-Apr-13 23:25
professionalАslam Iqbal23-Apr-13 23:25 
GeneralRe: My vote of 5 Pin
Morshed Anwar11-Jun-13 0:12
professionalMorshed Anwar11-Jun-13 0:12 
GeneralRe: My vote of 5 Pin
Аslam Iqbal11-Jun-13 0:19
professionalАslam Iqbal11-Jun-13 0:19 

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

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