Note: I use the preview version of Entity Framework Core 2.0 (2.0.0-preview2-final). Things may change after the final version is released
When you design a database, you sometimes need to add columns to track when a record changed and who made the change. To clarify, you add the following columns:
CreatedAtCreatedByLastUpdatedAtLastUpdatedBy
You can easily use default values and triggers to handle the CreatedAt and LastUpdatedAt columns. However, writing these triggers is tedious, and setting the user name is difficult because it is application-level information. In a web context, only one database user connects to the database, so you cannot rely on the CURRENT_USER function to store the actual application user.
Of course, you don't want to manage these properties manually. Instead, you want Entity Framework to handle them automatically. The solution is to set the values before calling SaveChanges or SaveChangesAsync. There are multiple approaches: read/write properties, read-only properties, or shadow properties. With shadow properties, the columns exist in the database but are not mapped to any property on the model - only Entity Framework knows about them. EF Core is very flexible in this regard.
Let's implement all 3 approaches!
#Read/Write properties
In this case, the entity has read/write properties, meaning their values can be changed by your code.
C#
public interface ITrackable
{
DateTime CreatedAt { get; set; }
string CreatedBy { get; set; }
DateTime LastUpdatedAt { get; set; }
string LastUpdatedBy { get; set; }
}
public class Post : ITrackable
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime CreatedAt { get; set; }
public string CreatedBy { get; set; }
public DateTime LastUpdatedAt { get; set; }
public string LastUpdatedBy { get; set; }
}
Now, override the SaveChanges method to set the tracking property values before saving. Note that both SaveChanges and the async SaveChangesAsync must be overridden. The code iterates over all entities tracked by the context and, depending on their state, sets the tracking property values:
C#
public class BloggingContext : DbContext
{
public DbSet<Post> Posts { get; set; }
public override int SaveChanges(bool acceptAllChangesOnSuccess)
{
OnBeforeSaving();
return base.SaveChanges(acceptAllChangesOnSuccess);
}
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
OnBeforeSaving();
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
private void OnBeforeSaving()
{
var entries = ChangeTracker.Entries();
foreach (var entry in entries)
{
if (entry.Entity is ITrackable trackable)
{
var now = DateTime.UtcNow;
var user = GetCurrentUser();
switch (entry.State)
{
case EntityState.Modified:
trackable.LastUpdatedAt = now;
trackable.LastUpdatedBy = user;
break;
case EntityState.Added:
trackable.CreatedAt = now;
trackable.CreatedBy = user;
trackable.LastUpdatedAt = now;
trackable.LastUpdatedBy = user;
break;
}
}
}
}
private string GetCurrentUser()
{
return "UserName"; // TODO implement your own logic
// If you are using ASP.NET Core, you should look at this answer on StackOverflow
// https://stackoverflow.com/a/48554738/2996339
}
}
The downside of this approach is that the properties have setters, even though you never intend to set them from your code. Let's see how to use read-only properties instead.
#Read-only properties
Entity Framework Core allows mapping a column to a field. This way, you can use read-only properties. Let's modify the Post class:
C#
public class Post
{
private DateTime _createdAt;
private string _createdBy;
private DateTime _lastUpdatedAt;
private string _lastUpdatedBy;
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime CreatedAt => _createdAt;
public string CreatedBy => _createdBy;
public DateTime LastUpdatedAt => _lastUpdatedAt;
public string LastUpdatedBy => _lastUpdatedBy;
}
This is a little more verbose, but the properties are now read-only. You need to instruct Entity Framework to use the fields to set the value of the column.
C#
public class BloggingContext : DbContext
{
public DbSet<Post> Posts { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Post>()
.Property(post => post.CreatedAt)
.HasField("_createdAt");
modelBuilder.Entity<Post>()
.Property(post => post.CreatedBy)
.HasField("_createdBy");
modelBuilder.Entity<Post>()
.Property(post => post.LastUpdatedAt)
.HasField("_lastUpdatedAt");
modelBuilder.Entity<Post>()
.Property(post => post.LastUpdatedBy)
.HasField("_lastUpdatedBy");
}
}
You cannot write the OnBeforeSaving method as before because there is no setter. Instead, you can use the ChangeTracker to indicate Entity Framework the value to set for the properties. This way there is no need to access the property or the field.
C#
private void OnBeforeSaving()
{
var entries = ChangeTracker.Entries();
foreach (var entry in entries)
{
if (entry.Entity is Post post)
{
var now = DateTime.UtcNow;
var user = GetCurrentUser();
switch (entry.State)
{
case EntityState.Modified:
entry.CurrentValues["LastUpdatedAt"] = now;
entry.CurrentValues["LastUpdatedBy"] = user;
break;
case EntityState.Added:
entry.CurrentValues["CreatedAt"] = now;
entry.CurrentValues["CreatedBy"] = user;
entry.CurrentValues["LastUpdatedAt"] = now;
entry.CurrentValues["LastUpdatedBy"] = user;
break;
}
}
}
}
}
I think this approach is clean and should be the preferred way to go.
#No property
In some cases, you may want to set column values without exposing properties in the model. For instance, you may expose the last updated date, but not who made the change. Entity Framework Core can handle this by using shadow properties. Shadow properties are declared in the OnModelCreating method, but does not exist in your classes. You can read and write the value of the shadow properties using the ChangeTracker.
C#
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
}
C#
public class BloggingContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Declare properties
modelBuilder.Entity<Post>().Property<DateTime>("CreatedAt");
modelBuilder.Entity<Post>().Property<string>("CreatedBy");
modelBuilder.Entity<Post>().Property<DateTime>("LastUpdatedAt");
modelBuilder.Entity<Post>().Property<string>("LastUpdatedBy");
}
private void OnBeforeSaving()
{
var entries = ChangeTracker.Entries();
foreach (var entry in entries)
{
if (entry.Entity is Post post)
{
var now = DateTime.UtcNow;
var user = GetCurrentUser();
switch (entry.State)
{
case EntityState.Modified:
entry.CurrentValues["LastUpdatedAt"] = now;
entry.CurrentValues["LastUpdatedBy"] = user;
break;
case EntityState.Added:
entry.CurrentValues["CreatedAt"] = now;
entry.CurrentValues["CreatedBy"] = user;
entry.CurrentValues["LastUpdatedAt"] = now;
entry.CurrentValues["LastUpdatedBy"] = user;
break;
}
}
}
}
}
#Conclusion
Using Entity Framework Core, you can map database tables to your object model in a way that keeps your classes clean. You can hide implementation details and avoid exposing properties that should not be part of your public API.
Do you have a question or a suggestion about this post? Contact me!