AuditTrail.Core
1.3.0
dotnet add package AuditTrail.Core --version 1.3.0
NuGet\Install-Package AuditTrail.Core -Version 1.3.0
<PackageReference Include="AuditTrail.Core" Version="1.3.0" />
<PackageVersion Include="AuditTrail.Core" Version="1.3.0" />
<PackageReference Include="AuditTrail.Core" />
paket add AuditTrail.Core --version 1.3.0
#r "nuget: AuditTrail.Core, 1.3.0"
#:package AuditTrail.Core@1.3.0
#addin nuget:?package=AuditTrail.Core&version=1.3.0
#tool nuget:?package=AuditTrail.Core&version=1.3.0
AuditTrail.Core
A plug-and-play .NET 8/9/10 library that automatically audits EF Core entity changes using a SaveChangesInterceptor. No SaveChanges overrides, no migrations, no schema management — just register and go.
Features
- Captures table name, action (Added/Modified/Deleted), primary key(s), old values, new values, user, and timestamp
- Creates and maintains the
AuditLogstable on startup using raw DDL — no consumer migrations required - Compatible with SQL Server, PostgreSQL, SQLite, and any other EF Core relational provider
- Startup connectivity check — logs a warning at startup if the audit database is unreachable
- Pluggable user resolution via
IAuditUserProvider— works in web apps, background services, and workers [AuditIgnore]attribute (or fluent config) to exclude entities or properties from audit capture[AuditMask]attribute (or fluent config) to redact sensitive fields with"[redacted]"- Configurable retry policy for transient audit write failures
- Indexed on
TimestampandTableNamefor efficient querying
Installation
dotnet add package AuditTrail.Core --version 1.2.0
Setup
Program.cs
using AuditTrail.Core;
// 1. Register audit services — pass your provider directly.
// The library does not reference any specific provider package.
builder.Services.AddAuditTrail(opts => opts.UseSqlServer(auditConnectionString));
// 2. Register your DbContext with the interceptor wired automatically.
builder.Services.AddDbContextWithAuditTrail<ApplicationDbContext>(
opts => opts.UseSqlServer(appConnectionString));
var app = builder.Build();
// 3. Optional: mark the dependency visibly in your middleware pipeline.
// Schema setup runs automatically via the hosted service regardless.
app.UseAuditTrail();
The library creates the AuditLogs table on first startup using provider-specific DDL (IF NOT EXISTS / CREATE TABLE IF NOT EXISTS). It also detects and upgrades the column type if you are upgrading from v1.0.x.
ApplicationDbContext.cs
No changes required — the interceptor is wired at the DI level:
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options) { }
// ... your DbSets
}
That's it. Every SaveChanges / SaveChangesAsync call is intercepted and produces a row in AuditLogs.
Legacy pattern (still supported)
If you can't use AddDbContextWithAuditTrail, inject IServiceProvider and call UseAuditTrail on the options builder:
public class ApplicationDbContext : DbContext
{
private readonly IServiceProvider _sp;
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, IServiceProvider sp)
: base(options) { _sp = sp; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseAuditTrail(_sp);
}
Optional features
Excluding entities and properties
Apply [AuditIgnore] to skip an entire entity class or a single property:
[AuditIgnore]
public class SessionToken { ... } // entire entity skipped
public class User
{
[AuditIgnore]
public string? InternalNote { get; set; } // omitted from OldValues/NewValues JSON
}
For types you don't own, use the fluent API:
builder.Services.AddAuditTrail(
opts => opts.UseSqlServer(connStr),
features => features
.Ignore<IdentityUserToken<string>>()
.Ignore<User>(u => u.ConcurrencyStamp));
Masking sensitive fields
Apply [AuditMask] to replace a value with "[redacted]" in the captured JSON:
public class User
{
[AuditMask]
public string? PasswordHash { get; set; }
[AuditMask]
public string? SecurityStamp { get; set; }
}
Result: {"PasswordHash":"[redacted]","SecurityStamp":"[redacted]","Email":"user@example.com"}
Or configure via fluent API:
features.Mask<User>(u => u.PasswordHash);
Both [AuditIgnore] and [AuditMask] are inherited, so attributes on base-class properties are detected even when EF Core lazy-loading proxies are in use.
Retry policy
Re-attempt failed audit writes on subsequent saves (entries are held in memory between saves):
builder.Services.AddAuditTrail(
opts => opts.UseSqlServer(connStr),
features => features.RetryFailedEntries(maxRetries: 3));
If all retries are exhausted, the entries are discarded and logged at Error level via ILogger<AuditTrailInterceptor>.
Custom user resolver
The default resolver reads HttpContext.User.Identity.Name and falls back to "Anonymous". Override it for worker services, background jobs, or multi-tenant scenarios by registering your own IAuditUserProvider before AddAuditTrail:
// Register first so TryAddSingleton inside AddAuditTrail keeps yours.
builder.Services.AddSingleton<IAuditUserProvider, WorkerJobUserProvider>();
builder.Services.AddAuditTrail(opts => opts.UseSqlServer(connStr));
public class WorkerJobUserProvider : IAuditUserProvider
{
private readonly ICurrentJobContext _job;
public WorkerJobUserProvider(ICurrentJobContext job) => _job = job;
public string GetCurrentUser() => _job.CurrentUser ?? "system";
}
Non-web consumers (console apps, worker services) should always register a custom provider — the default HTTP resolver returns "Anonymous" when there is no active HTTP context.
Querying audit logs
Inject AuditTrailDbContext wherever you need to query the audit history:
public class AuditService
{
private readonly AuditTrailDbContext _auditDb;
public AuditService(AuditTrailDbContext auditDb) => _auditDb = auditDb;
public Task<List<AuditLog>> GetHistoryAsync(string tableName) =>
_auditDb.AuditLogs
.Where(a => a.TableName == tableName)
.OrderByDescending(a => a.Timestamp)
.ToListAsync();
}
AuditLog schema
| Column | Type | Description |
|---|---|---|
Id |
long |
Auto-generated primary key |
TableName |
string |
Name of the affected table |
Action |
string |
Added, Modified, or Deleted |
PrimaryKey |
string |
JSON-serialized primary key value(s) |
OldValues |
string |
JSON snapshot of values before the change (null for Added) |
NewValues |
string |
JSON snapshot of values after the change (null for Deleted) |
UserName |
string |
Resolved from IAuditUserProvider, or "Anonymous" |
Timestamp |
DateTime |
UTC time of the change |
Upgrading from 1.1.x
No breaking changes. New in 1.2.0:
AuditLogsschema is now created via raw DDL instead of EF migrations — the table is created for fresh installs and the column type is upgraded automatically on existing databases. No action required.app.UseAuditTrail()extension added forIApplicationBuilder.[AuditMask]and[AuditIgnore]now work correctly through EF Core lazy-loading proxy types.- Concurrent audit writes are serialized to prevent retry-queue race conditions under load.
Upgrading from 1.0.x
AuditLog.Idchanged frominttolong. Any code assigningIdto anintvariable will produce a compile-time error — change those tolong.- The library upgrades the column type automatically on startup (SQL Server and PostgreSQL). SQLite requires no change (
INTEGERis already 64-bit). AddAuditTrailgained an optional second parameter in 1.1.0. Existing single-argument call sites require no changes.- EF migrations are no longer used. Any previously applied migration entries in
__EFMigrationsHistoryare harmless — the raw DDL setup is idempotent.
Notes
- Do not add
DbSet<AuditLog>to your ownDbContext— the library manages its ownAuditTrailDbContext. - The
AuditLogstable is created in the database configured inAddAuditTrail(which can be different from your application database). AuditTrailDbContextcan be injected directly to query audit logs.- Audit writes happen after the application transaction commits, in a separate
AuditTrailDbContext. If the audit write fails, it is logged atErrorlevel. UseRetryFailedEntriesto tolerate transient failures. - The library does not reference any specific EF Core provider package. Add
Microsoft.EntityFrameworkCore.SqlServer,Npgsql.EntityFrameworkCore.PostgreSQL, or whichever provider your project needs, and pass it via theAddAuditTraildelegate.
MIT License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.Http (>= 2.2.0)
- Microsoft.EntityFrameworkCore (>= 10.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.0)
-
net8.0
- Microsoft.AspNetCore.Http (>= 2.2.0)
- Microsoft.EntityFrameworkCore (>= 8.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 8.0.0)
-
net9.0
- Microsoft.AspNetCore.Http (>= 2.2.0)
- Microsoft.EntityFrameworkCore (>= 9.0.0)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 9.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.