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

Implementing Audit Trail using Entity Framework Part -1

By , 27 Mar 2009
 

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 while 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 implementation I am going to use the Entity framework’s caching Management that is called as “ObjectStateManager”.Using this Manager I will be capable to find out the object that is currently changed or added or deleted and resides in EF cache as Object state entry. In part 1, I just going to talk about creating audit trail objects using the object state entry. In Second Part I will talk about roll back feature of this audit trial.

Using the Code

First I make table audit trail in database

DataBaseDbAudit.JPG

For this table, I am going to make an Entity set in my conceptual level as

dbAuditEF.JPG

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 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 those are going to save in DB.Here its takes each entry from EF cache of state- Added or Deleted or Modified and call 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))
            {//is a normal entry, not a relationship
                DBAudit audit = this.AuditTrailFactory(stateEntryEntity, UserName);
                auditTrailList.Add(audit);
            }
        }

        if (auditTrailList.Count > 0)
        {
            foreach (var audit in auditTrailList)
            {//add all audits 
                this.AddToDBAudit(audit);
            }
        }
    }

And here “AuditTrailFactory” is Factory method to create dbaudit object.Specially for Modify state it keeps the modified properties and serialized as XML.So using these field 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)
    {//entry is Added 
        audit.NewData = GetEntryValueInString(entry, false);
        audit.Actions = AuditActions.I.ToString();
    }
    else if (entry.State == EntityState.Deleted)
    {//entry in deleted
        audit.OldData = GetEntryValueInString(entry, true);
        audit.Actions = AuditActions.D.ToString();
    }
    else
    {//entry is modified
        audit.OldData = GetEntryValueInString(entry, true);
        audit.NewData = GetEntryValueInString(entry, false);
        audit.Actions = AuditActions.U.ToString();

        IEnumerable<string> modifiedProperties = entry.GetModifiedProperties();
        //assing collection of mismatched Columns name as serialized string 
        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 hold all change defination.First I make a clone 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 have done 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)
            {
                //Get orginal value 
                setterValue = entry.OriginalValues[propName];
            }
            else
            {
                //Get orginal value 
                setterValue = entry.CurrentValues[propName];
            }
            //Find property to update 
            PropertyInfo propInfo = target.GetType().GetProperty(propName);
            //update property with orgibal value 
            if (setterValue == DBNull.Value)
            {//
                setterValue = null;
            }
            propInfo.SetValue(target, setterValue, null);
        }//end foreach

        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 have found in MSDN forum Post (Thanx toPatrick 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.

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
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5memberАslam Iqbal23 Apr '13 - 20:58 
Really great article.
QuestionFor EntityState.Deleted not workingmemberMember 998096417 Apr '13 - 23:03 
Dear Team,
 
This article is very great but i have one issue ,
when i m going to delete with audit tracking then its original values are blank while values exist into database.
Please rectify my problem...
QuestionSaving changes eventmemberjportelas31 Dec '12 - 6:38 
Hi Morshed,
 
Great article!
 
I have a (rookie) question. SavingChanges events is triggered when EF is trying to persist info to the storage, but what happens if the savechanges call fails? will the audit logs will be written anyway?
 
Thanks!
jportelas

AnswerRe: Saving changes eventmemberMorshed Anwar31 Dec '12 - 21:59 
That is why transaction should been declared while doing any CUD operation. So it will depend on the transaction.Commit method.
Md. Morshed Anwar | Senior Software Engineer
Adaptive Enterprise Limited
Blog: http://morshedanwar.wordpress.com/

GeneralMy vote of 5memberLee Keel21 Nov '12 - 7:45 
This is a very simple audit to implement and Morshed has done a great job of answering any and all questions! *****
GeneralRe: My vote of 5memberMorshed Anwar21 Nov '12 - 11:07 
Thanks a lot Lee Smile | :)
Md. Morshed Anwar | Senior Software Engineer
Adaptive Enterprise Limited
Blog: http://morshedanwar.wordpress.com/

BugCloneEntity throwing Out-of-memory errormemberLee Keel19 Nov '12 - 14:09 
I have some objects that are referenced by many-to-many situations and in some circumstances this could cause paths that would allow you to get back to the same object. For example:
 
document1 associated to node1 and node2
document2 associated to node1 and node2.
 
The problem is that when you call dcSer.WriteObject(memoryStream, obj); it throws an out of memory error. Any ideas how to fix this?
 
Other than this problem, this is an excellent way for doing auditing!!!
GeneralRe: CloneEntity throwing Out-of-memory errormemberMorshed Anwar19 Nov '12 - 22:29 
From my understanding, it occurs when the process is running out of virtual memory or there is not enough physical memory available in order to commit.In this particular method, reasons could be use of large data or using memory stream. You can try FileStream in place of MemoryStream. Write it into an temporary file and and then read it.

sealed class TempFile : IDisposable
{
    string path;
    public TempFile() : this(System.IO.Path.GetTempFileName()) { }
 
    public TempFile(string path)
    {
        if (string.IsNullOrEmpty(path)) throw new ArgumentNullException("path");
        this.path = path;
    }
    public string Path
    {
        get
        {
            if (path == null) throw new ObjectDisposedException(GetType().Name);
            return path;
        }
    }
    ~TempFile() { Dispose(false); }
    public void Dispose() { Dispose(true); }
    private void Dispose(bool disposing)
    {
        if (disposing)
        {
            GC.SuppressFinalize(this);                
        }
        if (path != null)
        {
            try { File.Delete(path); }
            catch { } // best effort
            path = null;
        }
    }
}
 
public EntityObject CloneEntity(EntityObject obj)
{
    using (var tmp = new TempFile())
    {  
     using (FileStream stream = File.OpenWrite(tmp.Path)) {    
      DataContractSerializer dcSer = new DataContractSerializer(obj.GetType());
      dcSer.WriteObject(stream , obj);
      stream.Position = 0;    
 
      EntityObject newObject = (EntityObject)dcSer.ReadObject(stream);
      return newObject;
     }   
    }
}
Md. Morshed Anwar | Senior Software Engineer
Adaptive Enterprise Limited
Blog: http://morshedanwar.wordpress.com/

GeneralRe: CloneEntity throwing Out-of-memory errormemberLee Keel20 Nov '12 - 6:55 
Thanks so much for your prompt reply. However, this did not fix my issue. I have tried the code you provided, but it still throws the OOM error. I have 16GB of ram, so it is not a matter of actual free memory. The problem comes into the fact that it is trying to traverse the entire object tree, I believe which is causing a circular reference.
 
If I am wrong, then please point me in the right direction for what may be going on. Thanks in advance for your time!
GeneralRe: CloneEntity throwing Out-of-memory error [modified]memberMorshed Anwar20 Nov '12 - 22:47 
you are right. On Circular reference, there is possibility of becoming out of memory since it becomes very large xml. First of all, I like to know whether you need this XML representation to save into the database or not...You can also keep it another representation you like. But If you need this kind of representation, then You can think of ignoring the parent reference in Child class while serializing using IgnoreDataMemberAttribute (or user XML serializer with XMLIgnoreAttribute) while cloning. Another way is that- you can think of which no of level that you want your object to serialize. If you just need the scaler properties , not the navigation properties, then you can create a new instance and then copy the scaler value (directly with some mapper of all possible type or dynamic through the reflection). Hope it helps.
 
Here is sample of cloning Entity object (with related properties) using reflection. You can modify it as you like :
///
    /// This class is used to store self references for
    /// back tracking
    ///
    public class SelfReferencesTracking
    {
        public string EntitySetName;
        public EntityObject NewEntityObject;
        public EntityKey OriginalKeys;
    }
    ///http://www.urmanet.ch/?tag=c-30
    ///
    /// Extension method class for the EntityObject class
    ///
    public static class EntityObjectExtension
    {
        //Enable tracking
        private static readonly List<SelfReferencesTracking> _tracking = new List<SelfReferencesTracking>();
 
        ///
        /// These method makes a 1:1 copy of the original entity object
        ///
        /// The original entity object /// The copied entity object
        public static EntityObject Clone(this EntityObject entityObject)
        {
            //Get constructor for new object
            var newEntityObject = entityObject.GetType().GetConstructor(
            new Type[0]).Invoke(new object[0]);
 
            _tracking.Add(new SelfReferencesTracking
            {
                EntitySetName = entityObject.EntityKey.EntitySetName,
                OriginalKeys = entityObject.EntityKey,
                NewEntityObject = (EntityObject)newEntityObject
            });
 
            //Copy all properties and its values of the given type
            var properties = entityObject.GetType().GetProperties();
            foreach (var property in properties)
            {
                try
                {
                    var propertyValue = property.GetValue(entityObject, null);
                    PropertyInfo myProperty = property;
                    if (entityObject.EntityKey.EntityKeyValues.Where(x => x.Key == myProperty.Name).Count() == 0)
                    {
                        //Ignore all properties of these types
                        if (property.PropertyType != typeof(EntityKey) &&
                        property.PropertyType != typeof(EntityState) &&
                        property.PropertyType != typeof(EntityReference<>))
                        {
                            //Check, if the property is a complex type (collection), in that
                            //case, some special calls are necessary
                            if (property.GetCustomAttributes(typeof(EdmRelationshipNavigationPropertyAttribute), false).Count() == 1)
                            {
                                //Check for self referencing entities
                                if (propertyValue.GetType() == entityObject.GetType())
                                {
                                    //Get the self referenced entity object
                                    var selfRefrencedEntityObject =
                                    (EntityObject)property.GetValue(entityObject, null);
 
                                    //This variable is used to store the new parent entity objects
                                    EntityObject newParentEntityObject = null;
 
                                    //This loops might be replaced by LINQ queries... I didn't try that
                                    foreach (var tracking in _tracking.Where(x => x.EntitySetName == selfRefrencedEntityObject.EntityKey.EntitySetName))
                                    {
                                        //Check, if the key is in the tracking list
                                        foreach (var newKeyValues in selfRefrencedEntityObject.EntityKey.EntityKeyValues)
                                        {
                                            //Iterate trough the keys and values
                                            foreach (var orgKeyValues in tracking.OriginalKeys.EntityKeyValues)
                                            {
                                                //The key is stored in the tracking list, which means, this is
                                                //the foreign key used by the self referencing property
                                                if (newParentEntityObject == null)
                                                {
                                                    if (orgKeyValues.Key == newKeyValues.Key &&
                                                    orgKeyValues.Value == newKeyValues.Value)
                                                    {
                                                        //Store the parent entity object
                                                        newParentEntityObject = tracking.NewEntityObject;
                                                    }
                                                }
                                                else
                                                {
                                                    break;
                                                }
                                            }
                                        }
 
                                        //Set the value to the new parent entity object
                                        property.SetValue(newEntityObject, newParentEntityObject, null);
                                    }
                                }
                                else
                                {
                                    //Entity collections are always generic
                                    if (propertyValue.GetType().IsGenericType)
                                    {
                                        //Don't include self references collection, e.g. Orders1, Orders2 etc.
                                        //Check for equality of the types (string comparison)
                                        if (!propertyValue.GetType().GetGenericArguments().First().FullName.Equals(entityObject.GetType().FullName))
                                        {
                                            //Get the entities of the given property
                                            var entities = (RelatedEnd)property.GetValue(entityObject, null);
 
                                            //Load underlying collection, if not yet done...
                                            if (!entities.IsLoaded) entities.Load();
 
                                            //Create a generic instance of the entities collection object
                                            var t = typeof(EntityCollection<>).MakeGenericType(
                                            new[] { property.PropertyType.GetGenericArguments()[0] });
 
                                            var newEntityCollection = Activator.CreateInstance(t);
 
                                            //Iterate trough the entities collection
                                            foreach (var entity in entities)
                                            {
                                                //Add the found entity to the dynamic generic collection
                                                var addToCollection = newEntityCollection.GetType().GetMethod("Add");
                                                addToCollection.Invoke(
                                                newEntityCollection,
                                                    //new object[] {(EntityObject) entity});
                                                new object[] { Clone((EntityObject)entity) });
                                            }
 
                                            //Set the property value
                                            property.SetValue(newEntityObject, newEntityCollection, null);
                                        }
                                    }
                                }
                            }
                            else
                            {
                                //Common task, just copy the simple type property into the new
                                //entity object
                                property.SetValue(newEntityObject, property.GetValue(entityObject, null), null);
                            }
                        }
                    }
                }
                catch (InvalidCastException ie)
                {
                    //Hmm, something happend...
                    Debug.WriteLine(ie.Message);
 
                    continue;
                }
                catch (Exception ex)
                {
                    //Hmm, something happend...
                    Debug.WriteLine(ex.Message);
 
                    continue;
                }
            }
 
            return (EntityObject)newEntityObject;
        }
}
Md. Morshed Anwar | Senior Software Engineer
Adaptive Enterprise Limited
Blog: http://morshedanwar.wordpress.com/


modified 21 Nov '12 - 5:27.

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

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130523.1 | Last Updated 27 Mar 2009
Article Copyright 2009 by Morshed Anwar
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid