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