One of the questions I often receive from business users, auditors, and even fellow developers is very simple:
“Who changed this record?”
The second question usually follows immediately
“When was it changed?”
Sometimes there is also a third question:
“What exactly was changed?”
At first glance, these questions seem easy to answer. However, when you start working on real business systems such as payment platforms, merchant management systems, leasing applications, financial systems, or internal enterprise applications, you quickly realize that answering these questions consistently is not always straightforward.
In this article, I want to share my experience implementing audit logging in Entity Framework Core (EF Core), why it become important in several projects I worked on, and how audit logs helped solve the actual business problems.
Why Audit Logging Matters
When I was younger as a developer, I mainly focused on making the application work. If users could create records, update records, and delete records successfully, I considered the feature complete.
Then one day a user reported:
“The merchant configuration changed yesterday. Nobody knows who changed it.”
Another time:
“The reserve percentage was updated unexpectedly. We need to know when the change happened.”
And another:
“This terminal assignment looks different from last week. Can you check who modified it?”
Suddenly the conversation was no longer about functionality.
It became about accountability.
The business needed visibility into system activities.
Auditors needed evidence.
Support teams needed investigation tools.
Management needed confidence that changes could be traced.
This is where audit logging becomes extremely valuable.
What is Audit Logging?
Audit logging is the process of recording important actions performed within a system.
Typically, we want to capture:
- Who performed the action
- When the action happened
- What action was performed
- What data changed
A simple audit record might look like this:
| Field | Old Value | New Value |
|---|---|---|
| ReservePercentage | 5 | 10 |
| HoldingPeriod | 90 | 180 |
This additional information becomes extremely useful during investigations.
Starting with Basic Auditing
The simplest audit implementation is adding common fields to your entities..
public class Merchant
{
public int Id { get; set; }
public string MerchantName { get; set; }
public DateTime CreatedDate { get; set; }
public string CreatedBy { get; set; }
public DateTime? UpdatedDate { get; set; }
public string UpdatedBy { get; set; }
}Whenever a record is created:
merchant.CreatedDate = DateTime.UtcNow;
merchant.CreatedBy = currentUser;Whenever a record is updated:
merchant.UpdatedDate = DateTime.UtcNow;
merchant.UpdatedBy = currentUser;This approach already provides valuable information.
You immediately know who created and updated the record.
For many systems, this may be sufficient.
However, eventually business requirements grow.
The Challenge with Simple Auditing
in one of my projects, we had a challenge involving configuration changes.
The system already stored CreatedBy and UpdatedBy.
Unfortunately, that was not enough.
Users wanted answer such as:
“What was the old value before the update?”
“Which field changed?”
“Was the reserve percentage updated or only the description?”
Our basic audit fields could not answer these questions.
We needed a more detailed audit trail.
Creating an Audit Log Table
A common approach is creating a dedicated audit table.
public class AuditLog
{
public long Id { get; set; }
public string UserName { get; set; }
public string Action { get; set; }
public string TableName { get; set; }
public string RecordId { get; set; }
public string OldValues { get; set; }
public string NewValues { get; set; }
public DateTime Timestamp { get; set; }
}This table becomes a historical record of changes.
Instead of only storing the latest state, we preserve the change history.
Using Change Tracker in EF Core
One of the things I really like in EF Core is the ChangeTracker.
It allows us to inspect entity changes before they are saved.
var entries = ChangeTracker.Entries()
.Where(e => e.State == EntityState.Modified);This gives us access to modified entities.
For each modified property:
foreach (var property in entry.Properties)
{
if (property.IsModified)
{
var oldValue = property.OriginalValue;
var newValue = property.CurrentValue;
}
}This is where audit logging becomes powerful.
We can compare old values and new values autimatically.
Capturing Changes During SaveChanges
A common technique is overriding SaveChanges.
public override int SaveChanges()
{
CreateAuditLogs();
return base.SaveChanges();
}Or for asynchronous operations:
public override async Task<int> SaveChangesAsync(
CancellationToken cancellationToken = default)
{
CreateAuditLogs();
return await base.SaveChangesAsync(cancellationToken);
}Before EF Core commits the transaction, we inspect the pending changes and create audit entries.
Record Property Changes
Suppose a merchant record changes.
Before:
{
"ReservePercentage": 5
}After:
{
"ReservePercentage": 10
}We can store both values.
var changes = new
{
Property = property.Metadata.Name,
OldValue = property.OriginalValue,
NewValue = property.CurrentValue
};Then serialize the result.
var json = JsonSerializer.Serialize(changes);This allows future investigation.
Real Business Example
I remember a situation where a configuration value was modified.
The business team reported different transaction behavior after the change.
Nobody remembered exactly when the update happened.
Without audit logging, the investigation would have involved searching emails, reviewing deployment notes, and interviewing multiple people.
Instead, the audit logs immediately showed:
- User who made the change
- Date and time
- Previous value
- New value
The root cause was identified in minutes.
This saved a significant amount of effort.
Querying Audit Logs
Once data is stored, reporting becomes easy.
Find all changes made by a user:
var logs = await context.AuditLogs
.Where(x => x.UserName == "jsmith")
.ToListAsync();Find changes performed today:
var today = DateTime.Today;
var logs = await context.AuditLogs
.Where(x => x.Timestamp >= today)
.ToListAsync();Find updates to merchant records:
var today = DateTime.Today;
var logs = await context.AuditLogs
.Where(x => x.Timestamp >= today)
.ToListAsync();These simple queries become extremely useful for support teams.
Projecting Audit Data
Sometimes we do not need every field.
Projection helps return only the required information.
var logs = await context.AuditLogs
.Select(x => new
{
x.UserName,
x.Action,
x.Timestamp
})
.ToListAsync();This reduces unnecessary data transfer.
Joining Audit Logs with Users
Many systems store user information seperately.
A join can provide richer reporting.
var result =
from audit in context.AuditLogs
join user in context.Users
on audit.UserName equals user.UserName
select new
{
user.FullName,
audit.Action,
audit.Timestamp
};This creates friendlier audit reports.
Filtering by Specific Actions
Many organizations want to focus only on important events.
For example:
var logs = await context.AuditLogs
.Where(x => x.Action == "Delete")
.ToListAsync();Or:
var logs = await context.AuditLogs
.Where(x => x.TableName == "Merchant")
.Where(x => x.Action == "Update")
.ToListAsync();These reports are often requested during audits.
Soft Delete and Audit Logging
Another lesson I learned over time is that deleting records permanently is often risky.
Instead of deleting:
context.Merchants.Remove(merchant);Many systems implement soft delete.
merchant.IsDeleted = true;Combined with audit logging:
audit.Action = "SoftDelete";This preserves history while hiding records from normal operations.
Capturing Login Activities
Audit logging is not limited to database updates.
Many systems also log:
- Login
- Logout
- Failed login attempts
- Password changes
- Permission updates
Example:
var audit = new AuditLog
{
UserName = currentUser,
Action = "Login",
Timestamp = DateTime.UtcNow
};This provides additional security visibility.
Handling Sensitive Data
One thing I learned is that not everything should be logged.
For example:
- Passwords
- Security Keys
- Tokens
- Personal Information
Avoid storing sensitive values directly.
Instead:
audit.NewValues = "[REDACTED]";This protects sensitive information while preserving the audit trail.
Performance Considerations
As systems grow, audit tables grow rapidly.
I have seen audit tables become much larger than business tables.
Some strategic include:
- Archiving old records
- Partitioning tables
- Creating indexes
- Logging only important entities
For example:
modelBuilder.Entity<AuditLog>()
.HasIndex(x => x.Timestamp);This improves search performance.
Multi User Investigation Scenario
One memorable challenge involved investigating a sequence of updates performed by multiple users.
The business wanted to reconstruct the timeline.
Using audit logs, we simple sorted records chronologically.
var history = await context.AuditLogs
.OrderBy(x => x.Timestamp)
.ToListAsync();The complete story became visible.
We could see:
- User A created the record
- User B updated configuration
- User C approved the change
Without audit logging, this investigation would have taken much longer.
Making Audit Logs Useful for Support Teams
One mistake I made early was focusing only on developers.
The support team also needs useful audit information.
Instead of storing cryptic messages:
Action = "U"Store meaningful values:
Action = "Merchant Configuration Updated"Human readable logs reduce investigation time significantly
Final Thoughts
Audit logging is one of those features that many teams postpone until a problem occurs.
I used to think the same way.
As long as records were saved correctly, everything seemed fine.
But after facing production investigations, audit requirements, configuration disputes, and support incidents, I learned that audit logging is not merely a technical feature.
It is a business feature.
It provides accoutability
It improves support operations.
It helps auditors.
It assists management.
Most importantly, it allows a teams to answer one of the most common questions in enterprise systems:
“Who did what and when?”
If you are building systems with EF Core, I strongly recommend implementing audit logging early. Even a simple implementation can save countless hours during troubleshooting and investigation later.
The effort invested today can prevent major headaches tomorrow.
Sources
The concepts discussed in this article are based on personal experience implementing enterprise applications and on information summarized from the following references:
- Microsoft Documentation. Entity Framework Core Change Tracking
- Microsoft Documentation. Saving Data in EF Core
- Microsoft Documentation. DbContext.SaveChanges Method
- Microsoft Documentation. EF Core Logging, Events and Diagnostics
- Microsoft Documentation. EF Core Querying Data
- Microsoft Documentation. EF Core Performance Considerations
- Microsoft Documentation. Global Query Filters
- Microsoft Documentation. EF Core Indexes






