One of the coolest features of EF Core is that you can define properties that don’t exist in the Entity
class. They are called shadow properties. While working on EF6, I needed some common audit fields that every entity will extend. Like, when a row of a table is updated, when a row is created, what is the row version, etc. Since there was no concept of shadow properties in EF6, what I and many other developers did is create an interface with those common fields and implement that interface in required entities. Then add or update the values of these fields through change tracking. Like this:
public interface IAuditable
{
DateTime Created { get; set; }
DateTime Modified { get; set; }
}
Implement the interface where it is needed:
public class Person : IAuditable
{
public int Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public DateTime Created { get; set; }
public DateTime Modified { get; set; }
}
Update values of the audit fields through entity change tracking:
public override int SaveChanges()
{
foreach (var auditableEntity in ChangeTracker.Entries<IAuditable>())
{
if (auditableEntity.State == EntityState.Added ||
auditableEntity.State == EntityState.Modified)
{
auditableEntity.Entity.Modified = DateTime.Now;
if (auditableEntity.State == EntityState.Added)
{
auditableEntity.Entity.Created = DateTime.Now;
}
}
}
return base.SaveChanges();
}
Instead of creating an interface and implementing it in every entity, we can also add the audit fields as shadow fields. It’s just another way, not a preferred way. So be very choosy when you are designing your entities.
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<Person>().Property<DateTime?>("Created");
builder.Entity<Person>().Property<DateTime?>("Modified");
}
Here, the question mark after the data types means that there can be a null
value for that mapped column type in the database. If you don't specify it, for datetime
column types, ef core will set a default datetime string
which sometimes can be confusing.
Since shadow properties are not directly part of the concrete entities, accessing them and working with their values is pretty much different from the usual way.
public override int SaveChanges()
{
var modifiedEntries = ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added || e.State == EntityState.Modified);
foreach (EntityEntry entry in modifiedEntries)
{
var entityType = entry.Context.Model.FindEntityType(entry.Entity.GetType());
var modifiedProperty = entityType.FindProperty("Modified");
var createdProperty = entityType.FindProperty("Created");
if (entry.State == EntityState.Modified && modifiedProperty != null)
{
entry.Property("Modified").CurrentValue = DateTime.Now;
}
if (entry.State == EntityState.Added && createdProperty != null)
{
entry.Property("Created").CurrentValue = DateTime.Now;
}
}
return base.SaveChanges();
}
And that’s it! This is how you can use shadow properties for implementing audit fields for your entities in a different kind of way. But it is up to you whether you want to use them or not.