A small, production-shaped reference implementation of a system-wide audit log for .NET + EF Core, with an Angular admin UI. Every create, update, and delete is captured automatically at the persistence layer, rendered as a field-level diff, and any deletion can be restored from its snapshot.
It doesn't just say something changed. It tells you exactly which field, and what it went from and to.
Most apps bolt on audit logging field by field, which is tedious, easy to forget, and drifts out of sync the moment someone adds a column. This project shows the better pattern: capture changes once, at the ORM boundary, so every mutation is logged the same way with zero per-feature wiring, and the full before/after snapshot is kept so a record is never truly lost.
It is intentionally small and dependency-light so you can read the whole thing in an afternoon and lift the mechanism into your own stack.
- Automatic capture of every Insert / Update / Delete via an EF Core
SaveChangesInterceptor. Add a column tomorrow, it is audited with no extra code. - Who / what / when on every entry: actor, action, entity, table, UTC timestamp.
- Before/after JSON snapshots, with the primary key and configurable sensitive fields excluded.
- Soft-delete awareness: an
IsActiveflip is recorded as a realDelete, not a confusingUpdate. - No-op suppression: saves that change nothing write nothing.
- Field-level diff UI: every change renders as a unified change-row (
field · old → new, with colorblind-safe+ / − / ~glyphs), and the classic side-by-side layout is one click away. - Three ways to read the log: Detailed (the full diff), Compact (one scannable line per change), and Timeline (grouped by day on a rail).
- One-click Restore: rebuild a deleted record from its snapshot. The restore is itself audited, so history stays append-only.
- Filter + export: filter by action, module, actor, and date, and export the view to CSV.
- Polished admin UI: light and dark themes, accent and density controls (persisted), styled dialogs and toasts, skeleton loading, and an optional guided tour. Angular standalone with signals, no UI library.
The entire mechanism is a single interceptor that runs inside the same transaction as your save:
┌──────────────┐ HTTP / JSON ┌─────────────────────────────────────┐
│ Angular UI │ ─────────────────▶ │ .NET Minimal API │
│ CRUD page │ │ │
│ Audit tab │ ◀───────────────── │ EF Core DbContext │
│ (diff + │ │ └── AuditSaveChangesInterceptor │
│ restore) │ │ • walk ChangeTracker │
└──────────────┘ │ • classify Insert/Update/ │
│ Delete (soft-delete aware)│
│ • serialize old/new JSON │
│ • append AuditTrail rows │
│ │ │
│ ▼ │
│ SQLite (data + audit) │
└─────────────────────────────────────┘
On every SaveChanges, the interceptor walks the change tracker, classifies each touched entity, serializes the relevant snapshot side(s), and appends an AuditTrail row, all before the transaction commits. See backend/Auditing/AuditSaveChangesInterceptor.cs.
📖 Want the full walkthrough (action classification, soft-delete handling, the restore flow, a sequence diagram, and how to port the pattern to another ORM)? See ARCHITECTURE.md.
You need the .NET 9 SDK and Node 20+.
# 1. Backend (http://localhost:5080)
cd backend
dotnet run
# 2. Frontend (http://localhost:4200), in a second terminal
cd frontend
npm install
npm startOr run both at once from the repo root:
make devThen open http://localhost:4200.
The app opens on Audit Trail, pre-seeded with a week of incident history (logged, escalated, resolved, and one soft-deleted as a duplicate). Then:
- On Incidents, create a record, edit its severity, and delete it.
- Back on Audit Trail you will see your
Insert,Update(only the changed field highlighted), andDelete(with the full prior snapshot). Switch between Detailed, Compact, and Timeline. - Click Restore on a
Deleterow. The record reappears in Incidents, and a newRestoreentry is logged.
Prefer a hands-off walkthrough? Click Tour in the top-right for a guided tour of the screen.
This repo is built to be picked up by a coding agent (Claude Code, Codex, Cursor, and friends). It ships an AGENTS.md (and a CLAUDE.md) that orient an agent in seconds: what the project is, how to run and test it, a map of the code, and the things to know before changing anything.
Clone the repo, open your terminal AI inside it, and paste this prompt:
You are picking up the Audit Trail repo. Read AGENTS.md and README.md, then the
core file backend/Auditing/AuditSaveChangesInterceptor.cs. Build and run it with
`make dev` (backend on :5080, frontend on :4200), then run the tests:
`dotnet test`, `cd frontend && npm run test:ci`, and `cd e2e && npm test`.
Summarize the architecture in a few bullets, then propose the next improvement
from the README roadmap. Do not change behavior until I approve the plan.
Base URL http://localhost:5080/api. Pass an optional X-Demo-User header to set the actor. An interactive API explorer (Scalar, backed by OpenAPI) is served at http://localhost:5080/scalar when the backend is running.
| Method | Path | Purpose |
|---|---|---|
GET |
/accidents |
List active records (paged) |
POST |
/accidents |
Create, writes an Insert audit entry |
PUT |
/accidents/{id} |
Update, writes an Update audit entry |
DELETE |
/accidents/{id} |
Soft-delete, writes a Delete audit entry |
GET |
/audit-trail |
List audit entries (filter: actionType, module, createdBy, createdDate, paged, sorted) |
POST |
/audit-trail/{id}/restore |
Restore a record from an entry's snapshot, writes a Restore entry |
audit-trail/
├── backend/ # .NET 9 minimal API + EF Core (SQLite)
│ ├── Auditing/
│ │ └── AuditSaveChangesInterceptor.cs ← the audit mechanism
│ ├── Data/AppDbContext.cs
│ ├── Domain/ # Accident, AuditTrail, ActionType
│ ├── Services/ # current-user stub
│ ├── Models/Contracts.cs # request/response DTOs
│ └── Program.cs # endpoints + DI + seed
├── frontend/ # Angular standalone app (signals, no UI library)
│ ├── src/styles.scss # design system: tokens, light/dark themes, components
│ └── src/app/
│ ├── pages/accidents/ # Incidents CRUD page
│ ├── pages/audit-trail/ # the trail: diff hero + Detailed/Compact/Timeline
│ ├── shared/ # diff engine, theming, toasts, dialogs, guided tour
│ └── services/ # typed API clients
├── backend.Tests/ # xUnit: interceptor + endpoint integration tests
└── e2e/ # Playwright end-to-end tests
- Revert an
Update(not just restore aDelete) - Pluggable storage (separate audit DB / schema), the production-grade isolation pattern
- XLSX export alongside CSV
- Real auth + per-permission gating for view / export / restore
- Postgres + SQL Server providers
- A "history timeline" view per record (all entries by
referenceCode)
Three layers, all runnable locally and in CI:
# Backend: unit tests for the audit interceptor + endpoint integration tests
dotnet test # from repo root (AuditTrail.sln)
# Frontend: component + service unit tests (headless, no browser needed)
cd frontend && npm run test:ci
# End-to-end: drives the real UI through create -> edit -> delete -> restore
cd e2e && npm install && npx playwright install chromium && npm test- Backend (
backend.Tests/, xUnit): proves theSaveChangesInterceptorclassifies Insert/Update/Delete (with the soft-delete override), excludes the primary key, skips no-ops, and that every endpoint writes the right audit entry, including restore's409/404cases. - Frontend (
frontend/src/**/*.spec.ts, Vitest): unit-tests the diff engine (computeDiffclassification, snapshot parsing, value formatting) and the typed API clients. - E2E (
e2e/, Playwright): runs the full demo flow in a real browser and asserts the audit trail records every step.npx playwright testboots both servers automatically; setE2E_BASE_URLto test an already-running stack.
Contributions are welcome. See CONTRIBUTING.md. Good first issues: add the XLSX exporter, add the per-record history view, or port the interceptor pattern to another ORM in a examples/ folder.
MIT © Joshua Bascos
