EF Core - Database Auditing with "UpdatedBy"

In this post we'll look at implementing Auditing in EF Core. specifically recording the last user to "touch" (create, update or delete) a row.
This is especially useful when there are multiple users who can edit the same records, it's helpful to provide an identifier of the last person who made changes to that data. Sometimes for compliance purposes and sometimes for "naming and shaming" 🤣

A Dirty Hack

At a recent job, I've seen this very functionality implemented sub-optimally. Username/UserId was passed from the WebApi Controller method, all the way down to the repository layer. Each method call had an "updatedBy" string in it's parameters. For myself and - I'm sure - many other devs, this screams "problem, please fix me". Each repository method also had the responsibility of taking this string and setting it to the correct property of the Database object being saved. Which, if forgotten, could mean you're losing vital auditing information in the system. And after a thorough inspection, it wasn't always added to the model being saved.

The Cleaner Solution

In an ideal world, each method being called all the way down to the repository, would not need to pass this username string to every child call. Instead the repository would be the only place where this data is cared about/saved.
By keeping this functionality localised to the repository, we can also reduce the likelyhood of somebody changing the username somewhere in the call stack, and possibly faking our audit logs.

My previous post about EF Core gave me a lot more understanding about the framework and after making the changes described there, I was able to see a really easy way to implement the Audit Tracking behaviour.

We'll define an interface which all models that we want to be Auditable will implement.

public interface IAuditable
{
    string UpdatedBy { get; set; }
}

In terms of EF Core, the responsibility of saving the Updating User would lie in the Context class. We'll overwrite SaveChanges again and ensure that any entity being modified, would have the auditing property set.

First off, we need somewhere to get the current user. For the current web project, that's all handled by our login system. We have a simple Interface (IUserService) which allows us to go and get the Username/UserId of the currently authenticated user.

It looks like this:

public interface IUserService
{
    string GetUserId();
    string GetUsername();
}

This is made available to the whole system via Dependency Injection. So when our DbContext class is instantiated, we'll require that we get given an IUserService too, in it's constructor.

private readonly IUserService _userService;

public OLTPContext(DbContextOptions options, IUserService userService) : base(options)
{
    _userService = userService;
}

Perfect, now the next bit is very simple and similar to the implementation in my previous EF Core blog post.

The DbContext Class overridden SaveChanges method looks like so:

public override int SaveChanges()
{
    ChangeTracker.DetectChanges();

    var modified = ChangeTracker.Entries().Where(x => x.State == EntityState.Added || x.State == EntityState.Modified || x.State == EntityState.Deleted);
    var updatingUser = _userService.GetUserName();

    foreach (var item in modified)
    {
    	if (item.Entity is IAuditable entity)
    	{
            item.CurrentValues[nameof(IAuditable.UpdatedBy)] = updatingUser;
        }
    }

    return base.SaveChanges();
}

Closing

It's really that simple, we've enabled Audit Logging for all entities in our database which require it. And we've not polluted the rest of the system, by passing strings around from the top of our call (up in the web/api controller), all the way through services and down to repositories.

It's all very clean and handled automatically in one central place.

In regards to the project where I implemented this, I spent a full day going through the whole system removing all the extraneous method parameters, also taking note that many of the repository methods weren't actually setting the UpdatedBy property. Reduced code and a more stable implementation, less prone to developer errors!

Win, win!

If you've got any questions/improvements on the above then please leave a comment or shout me on Twitter

ryansouthgate

Software developer, living in Coventry, loves .Net, JavaScript and learning new languages.

Coventry