Naar kennisoverzicht

Entity Framework Auditing

In a project I’m currently working on we wanted to log who changed what on particular data in our database. This will open up a lot of possibilities like proving who made a certain change, show your most recent changes and enable you to rollback changes to a previous point in time. There are off-course multiple options to solving this problem. We are using Entity framework and the DbContext has this ChangeTracker. We can use this in the SaveChanges method to nicely add this logic in a single place. I first added an interface to mark a model to be audited as shown below.
public interface IAuditable
{
}

You might not want to audit every property on a model. Therefor I added an attribute which can be used to exclude a property from being audited:

[AttributeUsage(AttributeTargets.Property)] 
public class DoNotAudit : Attribute 
{ 
}

A model using both the interface and the attribute could is shown below:

public class Book : IAuditable
{
   public decimal Id { get; set; }

   public string Title { get; set; }

   public string Author { get; set; }
      
   [DoNotAudit]
   public string SuperSecretValue { get; set; }
}

Next, we need a model to save our audit logs and add this as a DbSet to our context. This could be a model like this one:

public class Audit
{
   [Key]
   public decimal Id { get; set; }

   public string TableName { get; set; }

   public string PropertyName { get; set; }

   public string ChangeType { get; set; }

   public int EntityId { get; set; }
   
   public string OriginalValue { get; set; }

   public string NewValue { get; set; }

   public DateTime IsAuditedOn { get; set; }

   public string ModifiedBy { get; set; }
}

The last step is to implement the actual logic in the SaveChanges method in your context right before we save our changes. This following implementation searches for entries that implement the IAuditable interface. We will loop over all the properties of the entries that have been changed filtering out those that are marked with the DoNotAudit attribute.

   public class BookContext : DbContext
   {
      public BookContext()
         : base("name=BookContext")
      {
      }

      public DbSet<Audit> Audits { get; set; }

      public DbSet<Book> Books { get; set; }

      public override int SaveChanges()
      {
         UpdateAuditables();

         return base.SaveChanges();
      }

      private void UpdateAuditables()
      {
         var modifiedEntries = ChangeTracker.Entries<IAuditable>()
            .Where(c => c.State != EntityState.Unchanged && c.State != EntityState.Detached);

         foreach (var modifiedEntry in modifiedEntries)
         {
            var modifiedProperties = modifiedEntry.Entity.GetType()
               .GetProperties()
               .Where(prop => !Attribute.IsDefined(prop, typeof(DoNotAudit)))
               .ToList();

            foreach (var modifiedProperty in modifiedProperties)
            {
               var originalValue = modifiedEntry.OriginalValues[modifiedProperty.Name];
               var currentValue = modifiedEntry.CurrentValues[modifiedProperty.Name];

               if (!Equals(originalValue, currentValue))
               {
                  AddAuditChange(modifiedEntry, modifiedProperty.Name, originalValue, currentValue);
               }
            }
         }
      }

      private void AddAuditChange(DbEntityEntry<IAuditable> auditableEntity, string propertyName, object originalValue, object currentValue)
      {
         var audit = new Audit
         {
            TableName = auditableEntity.Entity.GetType().Name,
            EntityId = (int)auditableEntity.Entity.Id,
            ChangeType = auditableEntity.State.ToString(),
            PropertyName = propertyName,
            OriginalValue = auditableEntity.State == EntityState.Added ? "[NEW]" : originalValue.ToString(),
            NewValue = currentValue.ToString(),
            IsAuditedOn = DateTime.Now,
            ModifiedBy = Thread.CurrentPrincipal.Identity.Name
         };

         Set<Audit>().Add(audit);
      }
   }