With Entity Framework 6, we now have the ability to track changes to the database. EF6 DbContext contains a new property called ChangeTracker which you can hook up and discover all the changes. By overriding the SaveChanges method, we should be able to discover those changes before invoking the base SaveChanges function.
For auditing, one requirement is that we need to guarantee that the changes we make to the database as well as the audit information are saved in an atomic fashion. In addition, another requirement is the ability to track the primary key (or composite keys) of the object instance we are adding, updating or deleting. This, in the case of adding, is only available after SaveChanges is invoked for the actual changes we are making. As such, we will need to utilize a transaction scope to help us coordinate the changes.
The following is an example of some of the infrastructure code we need to create. The Change class is used to track the changes of the property that was changed, with Name storing the property name, OldValue storing the old value (if any) and CurrentValue storing the current value (if any).
We are making use of the AuditTracker to track the state of the entity before and after SaveChanges (in Instance property). This is to facilitate our use case where we need to capture the Id of the instance after it is saved to the database.
Lastly, the Audit class stores the actual audit information.
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Data.Entity; namespace EFChangeTracker { public class Change { public string Name { get; set; } public string OldValue { get; set; } public string CurrentValue { get; set; } } public class AuditTracker { public AuditTracker() { Changes = new List<Change>(); } public EntityState State { get; set; } public object Instance { get; set; } public List<Change> Changes { get; set; } } public class Audit { [Key] public int Id { get; set; } [StringLength(50)] public string Username { get; set; } [StringLength(50)] public string InstanceTypeName { get; set; } [StringLength(40)] public string InstanceId { get; set; } [StringLength(1000)] public string Change { get; set; } public DateTime? Created { get; set; } } }
We can see in the code below where SaveChanges is overridden. Our core changes are within the context of a transaction scope, which guarantees an atomic operation when persisting our changes as well as audit information. One interesting thing to note is that we are using the WindowsIdentity.GetCurrent() to get the user making the change. In the case of Single Sign On (SSO) or other authentication methods, we may want to decide how to get the "who" is making the change. In addition, we are using Reflection to get the properties with the Key attribute. Notice that we do account for the fact where we might have a composite primary keys by doing a further loop to concatenate the keys together in a single string.
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Data.Entity; using System.Linq; using System.Security.Principal; using System.Transactions; using Newtonsoft.Json; namespace EFChangeTracker { public class ExampleDatabase : DbContext { public ExampleDatabase() : base("Example") { } public DbSet<Person> Persons { get; set; } public DbSet<Audit> Audits { get; set; } public override int SaveChanges() { using (var trxn = new TransactionScope()) { PrepAudit(); var result = base.SaveChanges(); FinalizeAudit(); trxn.Complete(); return result; } } private readonly List>AuditTracker> _trackers = new List>AuditTracker>(); private void PrepAudit() { _trackers.Clear(); ChangeTracker.Entries().ToList().ForEach(change => { var tracker = new AuditTracker { Instance = change.Entity, State = change.State }; switch (change.State) { case EntityState.Added: change.CurrentValues.PropertyNames.ToList().ForEach(name => { tracker.Changes.Add(new Change { Name = name, CurrentValue = change.CurrentValues[name].ToString() }); }); break; case EntityState.Deleted: change.OriginalValues.PropertyNames.ToList().ForEach(name => { tracker.Changes.Add(new Change { Name = name, CurrentValue = change.OriginalValues[name].ToString() }); }); break; case EntityState.Modified: change.OriginalValues.PropertyNames.ToList().ForEach(name => { tracker.Changes.Add(new Change { Name = name, OldValue = change.OriginalValues[name].ToString(), CurrentValue = change.CurrentValues[name].ToString() }); }); break; } _trackers.Add(tracker); }); } private void FinalizeAudit() { _trackers.ForEach(track => { Audits.Add(new Audit { Username = WindowsIdentity.GetCurrent().Name, Change = JsonConvert.SerializeObject(track.Changes), Created = DateTime.UtcNow, InstanceTypeName = track.Instance.GetType().ToString(), InstanceId = GetInstanceId(track.Instance) }); }); base.SaveChanges(); } private string GetInstanceId(object o) { var keyProps = o.GetType().GetProperties().Where(x => x.GetCustomAttributes(typeof(KeyAttribute), false).Any()); var sb = new StringBuilder(); keyProps.ToList().ForEach(p => { sb.Append(p.GetValue(o).ToString() + "|"); }); return sb.ToString(); } } }
Comments
Post a Comment