-
Notifications
You must be signed in to change notification settings - Fork 0
Data Model
This page documents the audit data model — the objects that get persisted and the database schema they map to.
The top-level record. One transaction per SaveChanges call (or per manual pipeline push).
| Property | Type | Description |
|---|---|---|
Id |
Guid |
Unique identifier |
Timestamp |
DateTimeOffset |
When the operation happened (UTC) |
UserId |
string |
Who performed the action |
UserName |
string |
Display name of the user |
IpAddress |
string |
Client IP address |
CorrelationId |
string |
Request correlation ID for distributed tracing |
TraceId |
string |
Distributed trace ID |
Source |
string |
Application or service name |
GdprState |
GdprStorageState |
GDPR processing state of this transaction |
Metadata |
IDictionary<string, string> |
Custom key-value pairs (serialized as JSON) |
Entries |
ICollection<AuditEntry> |
The individual entity changes in this transaction |
One entry per entity that was changed (or read) within the transaction.
| Property | Type | Description |
|---|---|---|
Id |
Guid |
Unique identifier |
TransactionId |
Guid |
FK to parent transaction |
Action |
AuditAction |
Create (1), Read (2), Update (3), Delete (4) |
EntityName |
string |
Entity class name (e.g., "Order") |
EntityId |
string |
Primary key value(s). Composite keys joined with _
|
EntityTypeName |
string |
Full CLR type name |
Properties |
ICollection<AuditEntryProperty> |
Property-level changes |
One property per field that changed (or was captured for reads).
| Property | Type | Description |
|---|---|---|
PropertyName |
string |
Property/column name |
PropertyType |
string |
CLR type name (e.g., "System.String") |
OldValue |
string |
Previous value (null for Create) |
NewValue |
string |
New value (null for Delete) |
All tables use the configured schema (default: audit).
CREATE TABLE [audit].[AuditTransactions] (
[Id] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
[Timestamp] DATETIMEOFFSET NOT NULL,
[UserId] NVARCHAR(MAX) NULL,
[UserName] NVARCHAR(MAX) NULL,
[IpAddress] NVARCHAR(MAX) NULL,
[CorrelationId] NVARCHAR(MAX) NULL,
[TraceId] NVARCHAR(MAX) NULL,
[Source] NVARCHAR(MAX) NULL,
[GdprState] INT NOT NULL DEFAULT 0,
[Metadata] NVARCHAR(MAX) NULL -- JSON
);
CREATE INDEX [IX_AuditTransactions_Timestamp] ON [audit].[AuditTransactions] ([Timestamp]);
CREATE INDEX [IX_AuditTransactions_UserId] ON [audit].[AuditTransactions] ([UserId]);
CREATE INDEX [IX_AuditTransactions_CorrelationId] ON [audit].[AuditTransactions] ([CorrelationId]);CREATE TABLE [audit].[AuditEntries] (
[Id] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
[TransactionId] UNIQUEIDENTIFIER NOT NULL,
[Action] INT NOT NULL,
[EntityName] NVARCHAR(MAX) NULL,
[EntityId] NVARCHAR(MAX) NULL,
[EntityTypeName] NVARCHAR(MAX) NULL,
CONSTRAINT [FK_AuditEntries_AuditTransactions_TransactionId]
FOREIGN KEY ([TransactionId])
REFERENCES [audit].[AuditTransactions] ([Id])
ON DELETE CASCADE
);
CREATE INDEX [IX_AuditEntries_TransactionId] ON [audit].[AuditEntries] ([TransactionId]);
CREATE INDEX [IX_AuditEntries_EntityName] ON [audit].[AuditEntries] ([EntityName]);CREATE TABLE [audit].[AuditEntryProperties] (
[Id] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWSEQUENTIALID(),
[AuditEntryId] UNIQUEIDENTIFIER NOT NULL,
[PropertyName] NVARCHAR(MAX) NULL,
[PropertyType] NVARCHAR(MAX) NULL,
[OldValue] NVARCHAR(MAX) NULL,
[NewValue] NVARCHAR(MAX) NULL,
CONSTRAINT [FK_AuditEntryProperties_AuditEntries_AuditEntryId]
FOREIGN KEY ([AuditEntryId])
REFERENCES [audit].[AuditEntries] ([Id])
ON DELETE CASCADE
);
CREATE INDEX [IX_AuditEntryProperties_AuditEntryId] ON [audit].[AuditEntryProperties] ([AuditEntryId]);Deleting an AuditTransaction cascades to its AuditEntries, and deleting an AuditEntry cascades to its AuditEntryProperties. This means PurgeBeforeAsync only needs to delete transactions - entries and properties go with them.
In MongoDB, each AuditTransaction is a single document in the audit_transactions collection. Entries and properties are embedded arrays:
{
"_id": "550e8400-e29b-41d4-a716-446655440000",
"Timestamp": "2026-04-16T10:30:00+00:00",
"UserId": "user-42",
"UserName": "Alice",
"IpAddress": "192.168.1.10",
"CorrelationId": "abc-123",
"TraceId": "def-456",
"Source": "OrderService",
"GdprState": 0,
"Metadata": { "RequestPath": "/api/orders" },
"Entries": [
{
"Id": "660e8400-e29b-41d4-a716-446655440001",
"Action": 3,
"EntityName": "Order",
"EntityId": "42",
"EntityTypeName": "MyApp.Entities.Order",
"Properties": [
{
"PropertyName": "Status",
"PropertyType": "System.String",
"OldValue": "Pending",
"NewValue": "Shipped"
}
]
}
]
}No joins are needed - the entire audit trail for one operation is in a single document.
Each daily JSON file (audit-2026-04-16.json) contains a JSON array of transaction objects using the same structure as the object model above. The format is identical to what System.Text.Json serializes from AuditTransaction.
RzR.DataVigil · Source Code · NuGet Packages · Built with .NET Standard 2.1
Getting started
Core features
Reference
Resources