Entity Framework Core Creation and Update fields

Basically @Steve's approach is the way to go, but the current implementation of it makes it hard to unit test your project.

With a little bit of refactoring, you can make it unit test friendly and stay true to SOLID principles and encapsulation.

Here's a refactored version of Steve's example

public abstract class AuditableEntity
{
    public DateTime CreatedDate { get; set; }
    public string CreatedBy { get; set; }
    public DateTime UpdatedDate { get; set; }
    public string UpdatedBy { get; set; }
}

public class AuditableDbContext : DbContext
{
    protected readonly IUserService userService;
    protected readonly DbContextOptions options;
    protected readonly ITimeService timeService;

    public BaseDbContext(DbContextOptions options, IUserService userService, ITimeService timeService) : base(options)
    {
        userService = userService ?? throw new ArgumentNullException(nameof(userService));
        timeService = timeService ?? throw new ArgumentNullException(nameof(timeService));
    }

    public override int SaveChanges()
    {
        // get entries that are being Added or Updated
        var modifiedEntries = ChangeTracker.Entries()
                .Where(x => (x.State == EntityState.Added || x.State == EntityState.Modified));

        var identityName = userService.CurrentUser.Name;
        var now = timeService.CurrentTime;

        foreach (var entry in modifiedEntries)
        {
            var entity = entry.Entity as AuditableEntity;

            if (entry.State == EntityState.Added)
            {
                entity.CreatedBy = identityName ?? "unknown";
                entity.CreatedDate = now;
            }

            entity.UpdatedBy = identityName ?? "unknown";
            entity.UpdatedDate = now;
        }

        return base.SaveChanges();
    }
}

Now it's easy to mock time and user/principal for unit tests and model/domain/business layer is free of EF Core dependency, better encapsulating your domain logic way better.

Of course one could further refactor this to use a more modular approach by using strategy pattern, but that's out of scope. You can also use ASP.NET Core Boilerplate which also offers an implementation of an auditable (and soft delete) EF Core DbContext (here and here)


I have exactly the same layout as you with what I call "Audit" fields.

The way I solved this was to create a base abstract class called AuditableEntity to hold the properties themselves and expose a method called PrepareSave. Inside PrepareSave I set the values of the fields as required:

public abstract class AuditableEntity
{
    public DateTime CreatedDate { get; set; }
    public string CreatedBy { get; set; }
    public DateTime UpdatedDate { get; set; }
    public string UpdatedBy { get; set; }

    public virtual void PrepareSave(EntityState state)
    {
        var identityName = Thread.CurrentPrincipal.Identity.Name;
        var now = DateTime.UtcNow;

        if (state == EntityState.Added)
        {
            CreatedBy = identityName ?? "unknown";
            CreatedDate = now;
        }

        UpdatedBy = identityName ?? "unknown";
        UpdatedDate = now;
    }
}

I made PrepareSave virtual so I can override it in my entities if I want. You may need to change how you get the Identity depending on your implementation.

To call this, I overwrote SaveChanges on my DbContext and called PrepareSave on each entity that was being added or updated (which I got from the change tracker):

public override int SaveChanges()
{
    // get entries that are being Added or Updated
    var modifiedEntries = ChangeTracker.Entries()
            .Where(x => x.State == EntityState.Added || x.State == EntityState.Modified);

    foreach (var entry in modifiedEntries)
    {
        // try and convert to an Auditable Entity
        var entity = entry.Entity as AuditableEntity;
        // call PrepareSave on the entity, telling it the state it is in
        entity?.PrepareSave(entry.State);
    }

    var result = base.SaveChanges();
    return result;
}

Now, whenever I call SaveChanges on my DbContext (either directly or through a repository), any entity that inherits AuditableEntity will have it's audit fields set as necessary.