This is the first part of a two part series on how to do audit trail implementation that is capable of rolling back to a certain period. Using the Entity Framework's caching manager, I will find out the object that is currently changed or added or deleted and resides in EF cache as Object state entry.
Introduction
Entity framework keeps track for those entire objects and relationships which have been deleted, added and modified in the container. EF keeps the state of the object and holds the necessary change-information and all these track information for each object or relationship resides as “objectStateEntry
”. Using “ObjectStateManager
”, one can access all these change-information like object-state (added/modified/deleted), modified properties, original and current values and can easily do audit trail for those objects. To get the Rollback feature from that audit trail, we have to consider some issues. We have to maintain the order of entity graph during insertion and deletion. That means root entity has been inserted before the children and during deletion, we have to make it reverse. The most important issue is that we have to make sure that audit trail entry will be inserted according this order.
So now I am going to talk about audit trail implementation that’s capable to rollback to a certain period. To make such an implementation, I am going to use the Entity framework’s caching Management that is called as “ObjectStateManager
”. Using this Manager, I will be capable of finding out the object that is currently changed or added or deleted and resides in EF cache as Object state entry. In Part 1, I am just going to talk about creating audit trail objects using the object state entry. In Part 2, I will talk about rollback feature of this audit trial.
Using the Code
First, I make table audit trail in database:
For this table, I am going to make an Entity set in my conceptual level as:
In Entity Framework, to save my all changes into Db, we have call “Context.SaveChanges()
” and this Context
is a container that has been inherited from “ObjectContext
” Class. To create the Audit trail Objects for each “Modified
/Added
/Deleted
”, I am going to catch the event:
Context.SavingChanges +=new EventHandler(Context_SavingChanges);
In the sample program, I have done it with writing partial class:
public partial class AdventureWorksEntities
{ partial void OnContextCreated()
{
this.SavingChanges += new EventHandler(AdventureWorksEntities_SavingChanges);
}
void AdventureWorksEntities_SavingChanges(object sender, EventArgs e)
{
So in my “AdventureWorksEntities_SavingChanges
” method, I am going to create all “dbaudit
” objects that are going to save in DB. Here, it takes each entry from EF cache of state - Added or Deleted or Modified and calls a factory method to produce audit trail object.
public partial class AdventureWorksEntities
{
public string UserName { get; set; }
List<DBAudit> auditTrailList = new List<DBAudit>();
public enum AuditActions
{
I,
U,
D
}
partial void OnContextCreated()
{
this.SavingChanges += new EventHandler(AdventureWorksEntities_SavingChanges);
}
void AdventureWorksEntities_SavingChanges(object sender, EventArgs e)
{
IEnumerable<ObjectStateEntry> changes =
this.ObjectStateManager.GetObjectStateEntries(
EntityState.Added | EntityState.Deleted | EntityState.Modified);
foreach (ObjectStateEntry stateEntryEntity in changes)
{
if (!stateEntryEntity.IsRelationship &&
stateEntryEntity.Entity != null &&
!(stateEntryEntity.Entity is DBAudit))
{
DBAudit audit = this.AuditTrailFactory(stateEntryEntity, UserName);
auditTrailList.Add(audit);
}
}
if (auditTrailList.Count > 0)
{
foreach (var audit in auditTrailList)
{
this.AddToDBAudit(audit);
}
}
}
And here “AuditTrailFactory
” is a Factory
method to create dbaudit
object. Especially for Modify
state, it keeps the modified properties and serialized as XML. So using these fields, you can easily show the changes of modified object with doing any comparison of old and new data.
private DBAudit AuditTrailFactory(ObjectStateEntry entry, string UserName)
{
DBAudit audit = new DBAudit();
audit.AuditId = Guid.NewGuid().ToString();
audit.RevisionStamp = DateTime.Now;
audit.TableName = entry.EntitySet.Name;
audit.UserName = UserName;
if (entry.State == EntityState.Added)
{
audit.NewData = GetEntryValueInString(entry, false);
audit.Actions = AuditActions.I.ToString();
}
else if (entry.State == EntityState.Deleted)
{
audit.OldData = GetEntryValueInString(entry, true);
audit.Actions = AuditActions.D.ToString();
}
else
{
audit.OldData = GetEntryValueInString(entry, true);
audit.NewData = GetEntryValueInString(entry, false);
audit.Actions = AuditActions.U.ToString();
IEnumerable<string> modifiedProperties = entry.GetModifiedProperties();
audit.ChangedColumns = XMLSerializationHelper.XmlSerialize(
modifiedProperties.ToArray());
}
return audit;
}
Here “GetEntryValueInString
” is for creating XML text of previous or modified object. In Entity Framework, each entry holds all change definition. First, I make a clone of the current object. Using entry.GetModifiedProperties()
, I can get only modified properties of an object and using “OriginalValues
” and “CurrentValues
”, I can build myself the old data and new data. Factory has told me what that wants – old or new. At the end, I do XML serialized and return back XML string.
private string GetEntryValueInString(ObjectStateEntry entry, bool isOrginal)
{
if (entry.Entity is EntityObject)
{
object target = CloneEntity((EntityObject)entry.Entity);
foreach (string propName in entry.GetModifiedProperties())
{
object setterValue = null;
if (isOrginal)
{
setterValue = entry.OriginalValues[propName];
}
else
{
setterValue = entry.CurrentValues[propName];
}
PropertyInfo propInfo = target.GetType().GetProperty(propName);
if (setterValue == DBNull.Value)
{
setterValue = null;
}
propInfo.SetValue(target, setterValue, null);
}
XmlSerializer formatter = new XmlSerializer(target.GetType());
XDocument document = new XDocument();
using (XmlWriter xmlWriter = document.CreateWriter())
{
formatter.Serialize(xmlWriter, target);
}
return document.Root.ToString();
}
return null;
}
To clone the entity, I have used the method which I found in MSDN forum post (Thanks to Patrick Magee).
public EntityObject CloneEntity(EntityObject obj)
{
DataContractSerializer dcSer = new DataContractSerializer(obj.GetType());
MemoryStream memoryStream = new MemoryStream();
dcSer.WriteObject(memoryStream, obj);
memoryStream.Position = 0;
EntityObject newObject = (EntityObject)dcSer.ReadObject(memoryStream);
return newObject;
}
That is all for Part 1 where I just create the Audit trail objects for each CUD operation.
History
- 27th March, 2009: Initial version