Click here to Skip to main content
Licence CPOL
First Posted 27 Mar 2009
Views 62,728
Downloads 833
Bookmarked 75 times

Implementing Audit Trail using Entity Framework - Part 1

By Morshed Anwar | 27 Mar 2009
Implementing Audit Trail using Entity Framework's caching entries
 
Part of The SQL Zone sponsored by
See Also

1

2
1 vote, 4.0%
3
3 votes, 12.0%
4
21 votes, 84.0%
5
4.88/5 - 25 votes
1 removed
μ 4.77, σa 0.89 [?]

Introduction

Entity framework keeps track of 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 the track information for each object or relationship resides as objectStateEntry. Using ObjectStateManager, one can access all this 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 doing 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 to 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 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 II, we will talk about roll back feature of this audit trial.

Using the Code

First, I make the table audit trail in the 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 all my changes into database, we have to 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 by 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))
            {//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 a Factory method to create dbaudit object.Specially for the Modify state, it keeps the modified properties and is serialized as XML. So using these fields, you can easily show the changes of modified object by 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();
        //passing 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 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 have serialized XML 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 original value 
                setterValue = entry.OriginalValues[propName];
            }
            else
            {
                //Get original value 
                setterValue = entry.CurrentValues[propName];
            }
            //Find property to update 
            PropertyInfo propInfo = target.GetType().GetProperty(propName);
            //update property with original 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 an 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 post

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

Software Developer

Bangladesh Bangladesh

Member


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. (secure sign-in)
 
Search this forum  
 FAQ
    Noise  Layout  Per page   
  Refresh
GeneralRe: Audit Trial Error - Xml File Generation PinmemberMember 314120622:09 2 May '10  
GeneralRe: Audit Trial Error - Xml File Generation PinmemberMorshed Anwar22:08 4 May '10  
GeneralNice article PinmemberDonsw8:29 27 Apr '09  
GeneralRe: Nice article PinmemberMorshed Anwar21:16 27 Apr '09  
GeneralGood Work PinmemberRaghuramanKan4:45 10 Apr '09  
GeneralRe: Good Work PinmemberMorshed Anwar20:35 12 Apr '09  
RantDoes not work for inserts with Identity columns PinmemberJesseA11:41 6 Apr '09  
This code will not save the identity column value(s) with the insert audit record. This is a problem with EF in general, as the event model does not provide a hook into the data after the changes are committed.
GeneralRe: Does not work for inserts with Identity columns [modified] PinmemberMorshed Anwar1:45 7 Apr '09  
GeneralRe: Does not work for inserts with Identity columns Pinmemberbetopu14:49 29 Dec '09  
AnswerRe: Does not work for inserts with Identity columns PinmemberJustin Marshall19:44 4 May '10  
GeneralRe: Does not work for inserts with Identity columns PinmemberMartinSpasovski16:43 17 May '10  
GeneralRe: Does not work for inserts with Identity columns PinmemberMorshed Anwar3:32 20 May '10  
GeneralRe: Does not work for inserts with Identity columns PinmemberJordans17x11:05 23 Nov '10  
GeneralNice Article PinmemberRazan Paul (Raju)22:18 27 Mar '09  
GeneralRe: Nice Article PinmemberMorshed Anwar21:38 29 Mar '09  

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

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

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