I once woke up to a production bug where every order total became 0 at 3:07 AM. The only thing scarier than that number was the silence in the logs. No trail. No hint. Just wrong. That is when I swore to never ship a system that cannot answer three simple questions: who changed what and when.

In this post we will build a clean audit trail in EF Core using interceptors, compare it to overriding SaveChanges, look at package options, then wrap with performance and security tips that keep your database happy.

The plan

We will record an audit row for each property that changes. That gives you a robust history and avoids schema drift. It looks like this: entity name, primary key, property name, old value, new value, action, user, timestamp. Think of it like a very nosy assistant who never sleeps and writes everything down.

The audit record

We will keep the audit table simple on purpose. You can add JSON columns, correlation ids or IP addresses later.

public class AuditLog
{
    public int Id { get; set; }
    public string Entity { get; set; } = "";
    public string Key { get; set; } = "";
    public string Property { get; set; } = "";
    public string? OldValue { get; set; }
    public string? NewValue { get; set; }
    public string Action { get; set; } = "";
    public string User { get; set; } = "";
    public DateTime UtcTime { get; set; }
}

Getting the current user

Our interceptor will need a user id. In a web app you can use IHttpContextAccessor. In a console or background worker you can return a service account id.

public interface ICurrentUser
{
    string? Id();
}

public sealed class HttpContextCurrentUser : ICurrentUser
{
    private readonly IHttpContextAccessor _http;
    public HttpContextCurrentUser(IHttpContextAccessor http) => _http = http;
    public string? Id() => _http.HttpContext?.User?.Identity?.Name ?? "system";
}

This article will be available on April 13, 2026 at 8 AM Central Time US