Friday 23 December 2011

Data Auditing using E.F 4.1 Code First


Well....it's been along time since I have written on this blog...been really busy....but I guess busy is good....anyways...I am going to jump right into it...

Note: This article requires the developer to have some knowledge of the DbContext class within EF4.1 Code First.

I want to discuss or share some insight into keeping a audit log using E.F 4.1 code first API...Since the Db Context(Object context) keeps track of what has been changed(in the object graph)...I am sure you would want to know what was changed and who changed it for auditing purposes, security purposes etc....The DBContext property called ChangeTracker keeps a log of all entities that have been modified, added or deleted for that session. This property is only accessible through a derived class of DbContext.

 You would probably want some "prettier" way of collecting the information from the ChangeTracker property and storing it. In order to do this I went about creating a custom Audit object.
I first went about creating an IAudit interface to 'map' out exactly what I needed from an audit trail

public interface IAudit
{
string EntityName { get; set; }
Type EntityType { get; set; }
string PropertyName { get; set; }
string OriginalValue { get; set; }
string NewValue { get; set; }
object OriginalObject { get; set; }
object UpdatedObject { get; set; }
EntityState Action { get; set; }
       string User { get; set; }
       DateTime EditedDate { get; set; }
}

 Here is the implementation 

public class Audit : IAudit
{
        #region IAudit Members

        public string EntityName { get; set;}
        public Type EntityType { get; set; }
        public string PropertyName { get; set; }
        public string OriginalValue { get; set; }
        public string NewValue { get; set; }
        public object OriginalObject { get; set; }
        public object UpdatedObject { get; set; }
        public EntityState Action { get; set; }
 public string User { get; set; }
        public DateTime EditedDate { get; set; }

        #endregion
}


Looking at each property
  • EntityName  - actual name of the entity
  • EntityType  - The entity  Type, could be used for casting etc.
  • PropertyName  - The property that was modified
  • OriginalValue – If a property(field) within the changed entity was modified or deleted, this property will hold he original value
  • NewValue – If a property(field) within the changed entity was modified or added, this property will hold the new value
  • OriginalObject - This property will contain the entity before being modified
  • UpdatedObject - This property will contain the entity before being modified
  • EntityState Action – This property will contain the change action i.e. Added, Updated, Deleted etc.
  • User – This property is for storing the login name of the current user logged in

In my DbContext derived class I created a method called GetAudit()which will return a list of Audit types containing the information needed \ requested. What this method will do is access the ChangeTracker property and iterate through it to get the current changes of the context.
There are several entity states but I will only e concentrating on the following states : Added, Modified and Deleted (you may add on the others if you wish)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;
using System.Data;
using System.Data.Entity.Infrastructure;
using System.Data.Objects;


    public class Context : DbContext
    {

        public Context(string connectionString)
            : base(connectionString)
        {
        }

        public IList<IAudit> GetAudit()
        {
              var audits = new List<IAudit>();

            var dbChangeTracker = ChangeTracker;

            if (dbChangeTracker != null)
foreach (var entityEntry in dbChangeTracker.Entries()
.Where(c => c.State == EntityState.Modified || c.State == EntityState.Added || c.State == EntityState.Deleted))
                {
                   foreach (var audit in GetModifiedEntityValues(entityEntry))
                              audits.Add(audit);
                }

            return audits;

        }
    }

 
The reason for the foreach loop in the above retrieval of changed entities is that, when an entity is modified(updated) you would want to return the collection of properties that was modified and provide a audit object for each property. In my next method called GetModifiedEntityValues I also add to the audit object the entire entity that was changed together with its entire original state, using the properties OriginalObject and UpdatedObject(for those people who wants to use the entire changed or original objet at one time).


I will now create the GetModifiedEntityValues in a moment. This is the meat of the audit functionality. This method retrieves all modifications that you have specified and fills the Audit object with those values. Firstly we would create either an ‘if’ statement or a ‘switch’ statement for decision making on each type of entity modification or EntityState.


private List<Audit> GetModifiedEntityValues(DbEntityEntry entityEntry)
{
var audits = new List<Audit>();

switch (entityEntry.State)
{
case EntityState.Modified:
break;
case EntityState.Deleted:
break;
case EntityState.Added:
break;
}

return audits;
}
Lets start with beefing up the Modified section first.
private List<Audit> GetModifiedEntityValues(DbEntityEntry entityEntry)
{
var audits = new List<Audit>();

switch (entityEntry.State)
{
case EntityState.Modified:
foreach (string propertyName in EntityEntry.OriginalValues.PropertyNames)
{
if (!Equals(entityEntry.OriginalValues.GetValue<object>(propertyName),entityEntry.CurrentValues.GetValue<object>(propertyName)))
                        {
audits.Add(new Audit()
                            {
                                Action = entityEntry.State,

EntityType =     ObjectContext.GetObjectType(entityEntry.Entity.GetType()),//Allows you to get entity type from proxy
EntityName = entityEntry.Entity.GetType().Name.Contains("_") ?
entityEntry.Entity.GetType().Name.Substring(0, entityEntry.Entity.GetType().Name.IndexOf('_'))
                                                 : entityEntry.Entity.GetType().Name,

                                PropertyName = propertyName,

                               OriginalValue =   entityEntry.OriginalValues.GetValue<object>(propertyName) == null ? null : entityEntry.OriginalValues.GetValue<object>(propertyName).ToString(),

                                NewValue = entityEntry.CurrentValues.GetValue<object>(propertyName) == null ? null : entityEntry.CurrentValues.GetValue<object>(propertyName).ToString(),

                                UpdatedObject = entityEntry.CurrentValues.ToObject(),

                                OriginalObject = entityEntry.OriginalValues.ToObject(),
User = "", //This could be set by any custome or integrated security that has the current user logged in
                                EditedDate = DateTime.Now

                            });
                        }
}
break;
case EntityState.Deleted:
break;
case EntityState.Added:
break;
}

return audits;
}
Let’s run through the not so obvious pieces of code.
In order for me to get the entity type from the context I need to use a mechanism that will allow me to get the type even if you are using Proxies in EF. The ObjectContext allows me to do this. If you just use entityEntry.Entity.GetType, then this will not work when using proxies.
EntityType = ObjectContext.GetObjectType(entityEntry.Entity.GetType)
I yet again catered for proxy created instance when retrieving the entity’s name.
EntityName = entityEntry.Entity.GetType().Name.Contains("_") ?
                                                 entityEntry.Entity.GetType().Name.Substring(0, entityEntry.Entity.GetType().Name.IndexOf('_'))                                                 : entityEntry.Entity.GetType().Name,

There is one of 2 ways, either the way above or the one below, so use whichever one you prefer. I will use the one above purely for fun J
EntityName = ObjectContext.GetObjectType(entityEntry.Entity.GetType()).Name

What I have also found is a sort of discrepancy when using legacy DB’s and EF. If for example your DB field are of CHAR type and you send in empty strings from your front end and insert into empty char fields, EF picks it up as a change(technically it is, logically it’s not) and will then insert an entry into the ChangeTracker indicating that the entity has been modified because your entity property had a value of “    “ and now has a string value of  “”. In order to ‘fix’\workaround this problem, I added another piece of code within the Modified section of the switch statement.

object propertyValue = entityEntry.OriginalValues.GetValue<object>(propertyName);

if (propertyValue != null)
{
     Type propertyType = entityEntry.OriginalValues.GetValue<object>(propertyName).GetType();
     if (propertyType == typeof(string))
     {
        if (entityEntry.OriginalValues.GetValue<object>(propertyName).ToString().Trim()
            == entityEntry.CurrentValues.GetValue<object>(propertyName).ToString().Trim())
                                    continue;
     }
}
What the above code does is that it gets all string type properties and if the original value is and empty Char and the new value is an empty string, it does not collect any audit log for that property.
The complete code for the modified section is

private List<Audit> GetModifiedEntityValues(DbEntityEntry entityEntry)
{
            var audits = new List<Audit>();

            switch (entityEntry.State)
            {
                case EntityState.Modified:
                    foreach (string propertyName in entityEntry.OriginalValues.PropertyNames)
                    {

                        //FIX FOR CHAR AND STRING PROBLEM
                        object propertyValue = entityEntry.OriginalValues.GetValue<object>(propertyName);

                        if (propertyValue != null)
                        {
                            Type propertyType = entityEntry.OriginalValues.GetValue<object>(propertyName).GetType();
                            if (propertyType == typeof(string))
                            {
                                if (entityEntry.OriginalValues.GetValue<object>(propertyName).ToString().Trim()
                                  == entityEntry.CurrentValues.GetValue<object>(propertyName).ToString().Trim())
                                    continue;
                            }
                        }
                        //END

                        if (!Equals(entityEntry.OriginalValues.GetValue<object>(propertyName), entityEntry.CurrentValues.GetValue<object>(propertyName)))
                        {
                            audits.Add(new Audit()

                            {
                                Action = entityEntry.State,

                                EntityType = ObjectContext.GetObjectType(entityEntry.Entity.GetType()),//Allows you to get entity type from proxy

                                EntityName = entityEntry.Entity.GetType().Name.Contains("_") ?
                                                 entityEntry.Entity.GetType().Name.Substring(0, entityEntry.Entity.GetType().Name.IndexOf('_'))
                                                 : entityEntry.Entity.GetType().Name,
                                PropertyName = propertyName,
                                OriginalValue = entityEntry.OriginalValues.GetValue<object>(propertyName) == null ? null : entityEntry.OriginalValues.GetValue<object>(propertyName).ToString(),
                                NewValue = entityEntry.CurrentValues.GetValue<object>(propertyName) == null ? null : entityEntry.CurrentValues.GetValue<object>(propertyName).ToString(),
                                UpdatedObject = entityEntry.CurrentValues.ToObject(),
                                OriginalObject = entityEntry.OriginalValues.ToObject(),
User = "", //This could be set by any custome or integrated security that has the current user logged in
                                EditedDate = DateTime.Now

                            });
                        }
                    }
                    break;
                case EntityState.Deleted:
                    break;
                case EntityState.Added:
                    break;
            }

            return audits;
}

The next 2 switches Deleted and Added are straight forward

Entity Added :
case EntityState.Added:
         audits.Add(new Audit
        {
          Action = entityEntry.State,
          EntityType = ObjectContext.GetObjectType(entityEntry.Entity.GetType()),

                        EntityName = entityEntry.Entity.GetType().Name.Contains("_") ?
                                           entityEntry.Entity.GetType().Name.Substring(0, entityEntry.Entity.GetType().Name.IndexOf('_'))
                                         : entityEntry.Entity.GetType().Name,

          NewValue = "Entity has been added",

          PropertyName = "AllPropertiesAdded",

          UpdatedObject = entityEntry.CurrentValues.ToObject(),

          User = "", //This could be set by any custome or integrated security that has the current user logged in

          EditedDate = DateTime.Now

          });

          break;

Entity Deleted :

case EntityState.Deleted:
         audits.Add(new Audit
         {
           Action = entityEntry.State,

           EntityType = ObjectContext.GetObjectType(entityEntry.Entity.GetType()),

           EntityName = entityEntry.Entity.GetType().Name.Contains("_") ?
           entityEntry.Entity.GetType().Name.Substring(0, entityEntry.Entity.GetType().Name.IndexOf('_'))
           : entityEntry.Entity.GetType().Name,

           NewValue = "Entity has been deleted",

           PropertyName = "AllPropertiedDeleted",

           OriginalObject = entityEntry.OriginalValues.ToObject(),

           User = "", //This could be set by any custome or integrated security that has the current user logged in
 
           EditedDate = DateTime.Now

           });
           break;

Our entire code class :
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.Entity;
using System.Data;
using System.Data.Entity.Infrastructure;
using System.Data.Objects;

public class Context : DbContext
{
        public DbSet<Address> Addresses
        { get { return Set<Address>(); } }

        public Context(string connectionString)
            : base(connectionString)
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();

            modelBuilder.Configurations.Add(new AddressMapping());

            base.OnModelCreating(modelBuilder);
        }

        public IList<IAudit> GetAudit()
        {
              var audits = new List<IAudit>();

            var dbChangeTracker = ChangeTracker;

            if (dbChangeTracker != null)
foreach (var entityEntry in dbChangeTracker.Entries()
.Where(c => c.State == EntityState.Modified || c.State == EntityState.Added || c.State == EntityState.Deleted))
                {
                   foreach (var audit in GetModifiedEntityValues(entityEntry))
                              audits.Add(audit);
                }

            return audits;

        }

        private List<Audit> GetModifiedEntityValues(DbEntityEntry entityEntry)
        {
            var audits = new List<Audit>();

            switch (entityEntry.State)
            {
                case EntityState.Modified:
                    foreach (string propertyName in entityEntry.OriginalValues.PropertyNames)
                    {

                        //FIX FOR CHAR AND STRING PROBLEM
                        object propertyValue = entityEntry.OriginalValues.GetValue<object>(propertyName);

                        if (propertyValue != null)
                        {
                            Type propertyType = entityEntry.OriginalValues.GetValue<object>(propertyName).GetType();
                            if (propertyType == typeof(string))
                            {
                                if (entityEntry.OriginalValues.GetValue<object>(propertyName).ToString().Trim()
                                    == entityEntry.CurrentValues.GetValue<object>(propertyName).ToString().Trim())
                                    continue;
                            }
                        }
                        //END

                        if (!Equals(entityEntry.OriginalValues.GetValue<object>(propertyName), entityEntry.CurrentValues.GetValue<object>(propertyName)))
                        {
                            audits.Add(new Audit()

                            {
                                Action = entityEntry.State,

                                EntityType = ObjectContext.GetObjectType(entityEntry.Entity.GetType()),//Allows you to get entity type from proxy
                               
                                EntityName = entityEntry.Entity.GetType().Name.Contains("_") ?
                                                 entityEntry.Entity.GetType().Name.Substring(0, entityEntry.Entity.GetType().Name.IndexOf('_'))
                                                 : entityEntry.Entity.GetType().Name,
                                PropertyName = propertyName,
                                OriginalValue = entityEntry.OriginalValues.GetValue<object>(propertyName) == null ? null : entityEntry.OriginalValues.GetValue<object>(propertyName).ToString(),
                                NewValue = entityEntry.CurrentValues.GetValue<object>(propertyName) == null ? null : entityEntry.CurrentValues.GetValue<object>(propertyName).ToString(),
                                UpdatedObject = entityEntry.CurrentValues.ToObject(),
                                OriginalObject = entityEntry.OriginalValues.ToObject(),
                                User = "", //This could be set by any custome or integrated security that has the current user logged in
                                EditedDate = DateTime.Now

                            });
                        }
                    }
                    break;
                case EntityState.Deleted:
                    audits.Add(new Audit
                    {
                        Action = entityEntry.State,
                        EntityType = ObjectContext.GetObjectType(entityEntry.Entity.GetType()),
                        EntityName = entityEntry.Entity.GetType().Name.Contains("_") ?
                                           entityEntry.Entity.GetType().Name.Substring(0, entityEntry.Entity.GetType().Name.IndexOf('_'))
                                         : entityEntry.Entity.GetType().Name,
                        NewValue = "Entity has been deleted",
                        PropertyName = "AllPropertiedDeleted",
                        OriginalObject = entityEntry.OriginalValues.ToObject(),
                        User = "", //This could be set by any custome or integrated security that has the current user logged in
                        EditedDate = DateTime.Now
                    });
                    break;
                case EntityState.Added:
                    audits.Add(new Audit
                    {
                        Action = entityEntry.State,
                        EntityType = ObjectContext.GetObjectType(entityEntry.Entity.GetType()),
                        EntityName = entityEntry.Entity.GetType().Name.Contains("_") ?
                                           entityEntry.Entity.GetType().Name.Substring(0, entityEntry.Entity.GetType().Name.IndexOf('_'))
                                         : entityEntry.Entity.GetType().Name,
                        NewValue = "Entity has been added",
                        PropertyName = "AllPropertiesAdded",
                        UpdatedObject = entityEntry.CurrentValues.ToObject(),
                        User = "", //This could be set by any custom or integrated security that has the current user logged in
                                EditedDate = DateTime.Now

                    });
                    break;
            }

            return audits;
        }

}

Shoooo....and there you have it....an EF auditable DbContext J. Add it to your EF projects test it and see if it works for you J
Happy coding!!.... 
P.S...My apologies for the formatting..I am trying to find a better tool for it...if anyone out there can point me in the right direction..that would be great :