We've been searching for a way to audit changes made to our database, and one method we found was doing so via Entity Framework change tracking. Here's how to do it, what's required to set it up, and some drawbacks of this method.
Let's get started!
Background
Entity Framework tracks changes made to entities when those entities are loaded from the Context. So, if we load an entity and modify it, like so:
var movie = context.Movies.Find(2);
movie.Title = "The Great Gatsby";
context.SaveChanges()
When the call to SaveChanges is made, Entity Framework is aware of what values have changed, what the new values are, and what the original values are. We need to access this information to accomplish the change tracking we want.
Setup
For this demo, let's assume we have the following entities and context:
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
}
public class Actor
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
}
public class MoviesContext : DbContext
{
public virtual DbSet<Movie> Movies { get; set; }
public virtual DbSet<Actor> Actors { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//omitted for brevity
}
}
How can we record changes made to these entities automatically, via Entity Framework? Let's find out!
Implementing Change Tracking
For this demo, we're going to store all changes made to any entity in a single data table. Said table can be represented by this entity:
public class ChangeLog
{
public int Id { get; set; }
public string EntityName { get; set; }
public string PropertyName { get; set; }
public string PrimaryKeyValue { get; set; }
public string OldValue { get; set; }
public string NewValue { get; set; }
public DateTime DateChanged { get; set; }
}
Notice the property PrimaryKeyValue. Part of recording "which entity changed" is both which table changed (e.g. "Movie", "Actor", etc) and which specific record in that table changed (e.g. 1, 2, etc.). In our implementation, we will need both of these for proper, thorough auditing. Let's see how we can actually implement auditing using Entity Framework.
In order to get all the data we need to produce these change logs, we will follow five steps:
- Override the Context's SaveChanges Method
- Get the Change Details
- Get the Primary Key Values
- Save the Logs
Let's see what we need to accomplish, step-by-step.
Step 1: Override the Context's SaveChanges Method
public override int SaveChanges()
{
return base.SaveChanges();
}
This allows us to automatically generate change logs any time an entity is saved to our database.
Step 2: Get the Change Details
How can we get the actual changes that will occur for this execution of SaveChanges? We will use the ChangeTracker property of DbContext, like so:
List<DbEntityEntry> modifiedChanges = ChangeTracker.Entries().Where(x => x.State == EntityState.Modified).ToList();
So what is DbEntityEntry? That type represents the change that will be made to a particular entity, including the original and current values and the state (Added, Modified, Deleted, etc) of that entity. What we now have is a list of all the entities being modified by this context.
Using this list of changes, we can cycle through each change and determine what values are actually changing:
public override int SaveChanges()
{
var modifiedEntities = ChangeTracker.Entries()
.Where(p => p.State == EntityState.Modified).ToList();
var now = DateTime.UtcNow;
foreach (var change in modifiedEntities)
{
var entityName = change.Entity.GetType().Name;
var primaryKey = GetPrimaryKeyValue(change);
foreach(var prop in change.OriginalValues.PropertyNames)
{
var originalValue = change.OriginalValues[prop].ToString();
var currentValue = change.CurrentValues[prop].ToString();
if (originalValue != currentValue) //Only create a log if the value changes
{
//Create the Change Log
}
}
}
return base.SaveChanges();
}
That method now allows us to get most of the data we need to create a change log. However, we haven't implemented GetPrimaryKeyValue yet.
Step 3: Get the Primary Key Values
We'll use a private method in the DbContext to get the primary key values (taken from this wonderful StackOverflow answer):
object GetPrimaryKeyValue(DbEntityEntry entry)
{
var objectStateEntry = ((IObjectContextAdapter)this).ObjectContext.ObjectStateManager.GetObjectStateEntry(entry.Entity);
return objectStateEntry.EntityKey.EntityKeyValues[0].Value;
}
In order to retrieve the primary keys, we must cast our DbContext down to IObjectContextAdapter and query the ObjectStateManager. Once we have access to that manager, we can get the primary key value (note that this method assumes a single-column primary key, which is not necessarily a good real-world scenario, see Drawbacks).
Step 4: Save the Logs
All we have to do now is add the logs to the Context and save the changes.
public override int SaveChanges()
{
var modifiedEntities = ChangeTracker.Entries()
.Where(p => p.State == EntityState.Modified).ToList();
var now = DateTime.UtcNow;
foreach (var change in modifiedEntities)
{
var entityName = change.Entity.GetType().Name;
var primaryKey = GetPrimaryKeyValue(change);
foreach(var prop in change.OriginalValues.PropertyNames)
{
var originalValue = change.OriginalValues[prop].ToString();
var currentValue = change.CurrentValues[prop].ToString();
if (originalValue != currentValue)
{
ChangeLog log = new ChangeLog()
{
EntityName = entityName,
PrimaryKeyValue = primaryKey.ToString(),
PropertyName = prop,
OldValue = originalValue,
NewValue = currentValue,
DateChanged = now
};
ChangeLogs.Add(log);
}
}
}
return base.SaveChanges();
}
The Complete Code
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
}
public class Actor
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
}
public class ChangeLog
{
public int Id { get; set; }
public string EntityName { get; set; }
public string PropertyName { get; set; }
public string PrimaryKeyValue { get; set; }
public string OldValue { get; set; }
public string NewValue { get; set; }
public DateTime DateChanged { get; set; }
}
public class MoviesContext : DbContext
{
public virtual DbSet<Movie> Movies { get; set; }
public virtual DbSet<Actor> Actors { get; set; }
public virtual DbSet<ChangeLog> ChangeLogs { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
//omitted for brevity
}
public override int SaveChanges()
{
var modifiedEntities = ChangeTracker.Entries()
.Where(p => p.State == EntityState.Modified).ToList();
var now = DateTime.UtcNow;
foreach (var change in modifiedEntities)
{
var entityName = change.Entity.GetType().Name;
var primaryKey = GetPrimaryKeyValue(change);
foreach(var prop in change.OriginalValues.PropertyNames)
{
var originalValue = change.OriginalValues[prop].ToString();
var currentValue = change.CurrentValues[prop].ToString();
if (originalValue != currentValue)
{
ChangeLog log = new ChangeLog()
{
EntityName = entityName,
PrimaryKeyValue = primaryKey.ToString(),
PropertyName = prop,
OldValue = originalValue,
NewValue = currentValue,
DateChanged = now
};
ChangeLogs.Add(log);
}
}
}
return base.SaveChanges();
}
}
Drawbacks
There are a couple of significant drawbacks to this particular solution:
- No auditing for Added entities. This is because, in my system, the database is responsible for creating the primary key values (via IDENTITY columns) and therefore the primary keys do not exist before the entity is added to the database. Attempting to use the database-generated primary keys for Added entities would result in two round-trips to the database on every save.
- Support for single-column primary keys only. This code makes an explicit assumption that only one column per table in your database is the primary key, which is not true in the real world.
Summary
The method outlined about is a good way to track and audit changes made to existing entities. It allows you to record the entity changed, the value changed, the primary key of the changed record, and the date of change. However, it does not track the adding of entities and it does not support multiple-column primary keys. With all of that said, I feel this is a pretty good step toward having full auditing in Entity Framework for our system.
Did I miss something, or can you improve my code? Please feel free to do so, and share in the comments!
Happy Coding!