In any database schema, it’s extremely common to have the fields “DateCreated, DateUpdated and DateDeleted” on almost every entity. At the very least, they provide helpful debugging information, but further, the DateDeleted affords a way to “soft delete” entities without actually deleting them.
That being said, over the years I’ve seen some pretty interesting ways in which these have been implemented. The worst, in my view, is writing C# code that specifically updates the timestamp when created or updated. While simple, one clumsy developer later and you aren’t recording any timestamps at all. It’s very prone to “remembering” that you have to update the timestamp. Other times, I’ve seen database triggers used which.. works.. But then you have another problem in that you’re using database triggers!
There’s a fairly simple method I’ve been using for years and it involves utilizing the ability to override the save behaviour of Entity Framework.
Auditable Base Model
The first thing we want to do is actually define a “base model” that all entities can inherit from. In my case, I use a base class called “Auditable” that looks like so :
public abstract class Auditable { public DateTimeOffset DateCreated { get; set; } public DateTimeOffset? DateUpdated { get; set; } public DateTimeOffset? DateDeleted { get; set; } }
And a couple of notes here :
- It’s an abstract class because it should only ever be inherited from
- We use DateTimeOffset because we will then store the timezone along with the timestamp. This is a personal preference but it just removes all ambiguity around “Is this UTC?”
- DateCreated is not null (Since anything created will have a timestamp), but the other two dates are! Note that if this is an existing database, you will need to allow nullables (And work out a migration strategy) as your existing records will not have a DateCreated.
To use the class, we just need to inherit from it with any Entity Framework model. For example, let’s say we have a Customer object :
public class Customer : Auditable { public int Id { get; set; } public string Name { get; set; } }
So all the class has done is mean we don’t have to copy and paste the same 3 date fields everywhere, and that it’s enforced. Nice and simple!
Overriding Context SaveChanges
The next thing is maybe controversial, and I know there’s a few different ways to do this. Essentially we are looking for a way to say to Entity Framework “Hey, if you insert a new record, can you set the DateCreated please?”. There’s things like Entity Framework hooks and a few nuget packages that do similar things, but I’ve found the absolute easiest way is to simply override the save method of your database context.
The full code looks something like :
public class MyContext: DbContext { public override Task<int> SaveChangesAsync(CancellationToken cancellationToken = default) { var insertedEntries = this.ChangeTracker.Entries() .Where(x => x.State == EntityState.Added) .Select(x => x.Entity); foreach(var insertedEntry in insertedEntries) { var auditableEntity = insertedEntry as Auditable; //If the inserted object is an Auditable. if(auditableEntity != null) { auditableEntity.DateCreated = DateTimeOffset.UtcNow; } } var modifiedEntries = this.ChangeTracker.Entries() .Where(x => x.State == EntityState.Modified) .Select(x => x.Entity); foreach (var modifiedEntry in modifiedEntries) { //If the inserted object is an Auditable. var auditableEntity = modifiedEntry as Auditable; if (auditableEntity != null) { auditableEntity.DateUpdated = DateTimeOffset.UtcNow; } } return base.SaveChangesAsync(cancellationToken); } }
Now you’re context may have additional code, but this is the bare minimum to get things working. What this does is :
- Gets all entities that are being inserted, checks if they inherit from auditable, and if so set the Date Created.
- Gets all entities that are being updated, checks if they inherit from auditable, and is so set the Date Updated.
- Finally, call the base SaveChanges method that actually does the saving.
Using this, we are essentially intercepting when Entity Framework would normally save all changes, and updating all timestamps at once with whatever is in the batch.
Handling Soft Deletes
Deletes are a special case for one big reason. If we actually try and call delete on an entity in Entity Framework, it gets added to the ChangeTracker as… well… a delete. And to unwind this at the point of saving and change it to an update would be complex.
What I tend to do instead is on my BaseRepository (Because.. You’re using one of those right?), I check if an entity is Auditable and if so, do an update instead. The copy and paste from my BaseRepository looks like so :
public async Task<T> Delete(T entity) { //If the type we are trying to delete is auditable, then we don't actually delete it but instead set it to be updated with a delete date. if (typeof(Auditable).IsAssignableFrom(typeof(T))) { (entity as Auditable).DateDeleted = DateTimeOffset.UtcNow; _dbSet.Attach(entity); _context.Entry(entity).State = EntityState.Modified; } else { _dbSet.Remove(entity); } return entity; }
Now your mileage may vary, especially if you are not using the Repository Pattern (Which you should be!). But in short, you must handle soft deletes as updates *instead* of simply calling Remove on the DbSet.
Taking This Further
What’s not shown here is that we can use this same methodology to update many other “automated” fields. We use this same system to track the last user to Create, Update and Delete entities. Once this is up and running, it’s often just a couple more lines to instantly gain traceability across every entity in your database!
Don’t override the `SaveChangesAsync` method anymore. you can write an `SaveChangesInterceptor` for it.
public class AuditableEntitiesInterceptor : SaveChangesInterceptor
Nice! Just had a look and this is definitely the way. This was introduced in .NET 5 for anyone reading this. So anything lower than that you’ll still have to override.
Hi,
about this sentence:
“We use DateTimeOffset because we will then store the timezone along with the timestamp. This is a personal preference but it just removes all ambiguity around “Is this UTC?””
The documentation says:
https://docs.microsoft.com/en-us/dotnet/api/system.datetimeoffset?view=net-6.0
Although a DateTimeOffset value includes an offset, it is not a fully time zone-aware data structure. While an offset from UTC is one characteristic of a time zone, it does not unambiguously identify a time zone. Not only do multiple time zones share the same offset from UTC, but the offset of a single time zone changes if it observes daylight saving time. This means that, as soon as a DateTimeOffset value is disassociated from its time zone, it can no longer be unambiguously linked back to its original time zone.
Also this page shows examples:
https://docs.microsoft.com/en-us/dotnet/standard/datetime/choosing-between-datetime
// 6/10/2007 12:00:00 AM -07:00 could belong to the following time zones:
// (GMT-07:00) Arizona
// (GMT-08:00) Pacific Time (US & Canada)
// (GMT-08:00) Tijuana, Baja California
Regards
Very true, timezone is probably a very poor choice of words there.
You’ll see that when I log the DateCreated/Updated/Deleted, I actually use DateTimeOffset.UtcNow. So the time logged will be +00:00. So specifically for me, I want someone looking at the database to *know* that the time is UTC, without looking at application code to check like they would have to if it was purely DateTime.
How about using ‘OfType()’ instead of checking every time whether entity is Auditable?