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:
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 is going to provide us:
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.
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
.
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 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.
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.
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 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