Introduction
Audit trails are crucial for tracking changes in data, maintaining security, and ensuring compliance with regulations. In this article, we will implement an audit trail in an ASP.NET Core Web API. The example will cover everything from setting up the project to performing CRUD operations and verifying the audit logs.
Prerequisites
- Visual Studio or Visual Studio Code
- SQL Server (or a suitable SQL database)
Step 1. Create a New ASP.NET Core Web API Project
Open your terminal or command prompt and run the following command to create a new ASP.NET Core Web API project:
dotnet new webapi -n AuditTrailExample
cd AuditTrailImplementtionInAspNetCoreWebAPI
Step 2. Install Required NuGet Packages
Install the necessary packages for Entity Framework Core and SQL Server.
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.AspNetCore.Http.Abstractions
Step 3. Define the Data Models
Create the Product and AuditLog entities.
Product Entity
Create a new folder Models and add the Product class.
namespace AuditTrailImplementtionInAspNetCoreWebAPI.Model
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}
}
AuditLog Entity
Add the AuditLog class.
namespace AuditTrailImplementtionInAspNetCoreWebAPI.Model
{
public class AuditLog
{
public int Id { get; set; }
public string? UserId { get; set; }
public DateTime Timestamp { get; set; }
public string? Action { get; set; }
public string? TableName { get; set; }
public string? RecordId { get; set; }
public string? Changes { get; set; }
}
}
Step 4. Configure the Database Context
Create a new folder Data and add the ApplicationDbContext class:
using AuditTrailImplementtionInAspNetCoreWebAPI.Model;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore;
using System.Security.Claims;
using System.Collections.Generic;
namespace AuditTrailImplementtionInAspNetCoreWebAPI.Data
{
public class ApplicationDbContext : DbContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IHttpContextAccessor httpContextAccessor)
: base(options)
{
_httpContextAccessor = httpContextAccessor;
}
public DbSet<AuditLog> AuditLogs { get; set; }
public DbSet<Product> Products { get; set; }
public override int SaveChanges()
{
var auditEntries = OnBeforeSaveChanges();
var result = base.SaveChanges();
OnAfterSaveChanges(auditEntries);
return result;
}
private List<AuditEntry> OnBeforeSaveChanges()
{
ChangeTracker.DetectChanges();
var auditEntries = new List<AuditEntry>();
var userId = _httpContextAccessor.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
foreach (var entry in ChangeTracker.Entries())
{
if (entry.Entity is AuditLog || entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)
{
continue;
}
var auditEntry = new AuditEntry(entry)
{
TableName = entry.Entity.GetType().Name,
Action = entry.State.ToString(),
UserId = "1234"
};
auditEntries.Add(auditEntry);
foreach (var property in entry.Properties)
{
string propertyName = property.Metadata.Name;
if (property.IsTemporary)
{
auditEntry.TemporaryProperties.Add(property);
continue;
}
if (entry.State == EntityState.Added)
{
auditEntry.NewValues[propertyName] = property.CurrentValue;
}
else if (entry.State == EntityState.Deleted)
{
auditEntry.OldValues[propertyName] = property.OriginalValue;
}
else if (entry.State == EntityState.Modified && property.IsModified)
{
auditEntry.OldValues[propertyName] = property.OriginalValue;
auditEntry.NewValues[propertyName] = property.CurrentValue;
}
}
}
foreach (var auditEntry in auditEntries.Where(e => !e.HasTemporaryProperties))
{
AuditLogs.Add(auditEntry.ToAuditLog());
}
return auditEntries.Where(e => e.HasTemporaryProperties).ToList();
}
private void OnAfterSaveChanges(List<AuditEntry> auditEntries)
{
if (auditEntries == null || auditEntries.Count == 0)
{
return;
}
foreach (var auditEntry in auditEntries)
{
foreach (var prop in auditEntry.TemporaryProperties)
{
if (prop.Metadata.IsPrimaryKey())
{
auditEntry.KeyValues[prop.Metadata.Name] = prop.CurrentValue;
}
else
{
auditEntry.NewValues[prop.Metadata.Name] = prop.CurrentValue;
}
}
AuditLogs.Add(auditEntry.ToAuditLog());
}
SaveChanges();
}
}
}

Join the conversation! Your thoughts help the community grow.