Headless.AuditLog.EntityFramework
0.4.15
dotnet add package Headless.AuditLog.EntityFramework --version 0.4.15
NuGet\Install-Package Headless.AuditLog.EntityFramework -Version 0.4.15
<PackageReference Include="Headless.AuditLog.EntityFramework" Version="0.4.15" />
<PackageVersion Include="Headless.AuditLog.EntityFramework" Version="0.4.15" />
<PackageReference Include="Headless.AuditLog.EntityFramework" />
paket add Headless.AuditLog.EntityFramework --version 0.4.15
#r "nuget: Headless.AuditLog.EntityFramework, 0.4.15"
#:package Headless.AuditLog.EntityFramework@0.4.15
#addin nuget:?package=Headless.AuditLog.EntityFramework&version=0.4.15
#tool nuget:?package=Headless.AuditLog.EntityFramework&version=0.4.15
Headless.AuditLog.EntityFramework
EF Core implementation of the audit log subsystem: change capture, persistent storage, and explicit event logging.
Problem Solved
Wires the audit log pipeline into EF Core's ChangeTracker so that entity mutations are captured and persisted atomically with the originating SaveChanges call — no separate commit, no data loss on rollback.
Key Features
EfAuditChangeCapture- Scans ChangeTracker before save; producesAuditLogEntryDataper changed entityEfAuditLogStore- AddsAuditLogEntryrows to the same DbContext so they commit in the same transactionEfAuditLog<TContext>- ImplementsIAuditLog<TContext>for explicit event logging (reads, PII reveals, failures)EfReadAuditLog<TContext>- ImplementsIReadAuditLog<TContext>for filtered read-back without leaking EF entitiesAuditLogEntry- Single-table entity with JSON columns forOldValues,NewValues, andChangedFieldsConfigureAuditLog()- ModelBuilder extension; supports custom table name, schema, and JSON column type- Soft-delete detection: automatically emits
entity.soft_deleted/entity.restoredactions whenIsDeletedtransitions - Suspend detection: emits
entity.suspended/entity.unsuspendedwhenIsSuspendedtransitions EntityFilterandPropertyFilterresults are cached after first evaluation for the capture service lifetime- Zero overhead when
AuditLogOptions.IsEnabledisfalse
Installation
dotnet add package Headless.AuditLog.EntityFramework
Quick Start
DI setup
services.AddHeadlessAuditLog(o =>
{
o.SensitiveDataStrategy = SensitiveDataStrategy.Redact;
});
services.AddHeadlessAuditLogEntity<AppDbContext>();
AddHeadlessAuditLogEntity requires AddHeadlessAuditLog (from Headless.AuditLog.Abstractions) to be called first for options registration, and AddHeadlessDbContext<T> for the DbContext registration.
DbContext setup
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ConfigureAuditLog();
}
Entity opt-in
public class Patient : AggregateRoot<Guid>, IAuditTracked
{
public string Name { get; set; } = "";
[AuditSensitive]
public string NationalId { get; set; } = "";
[AuditIgnore]
public DateTime LastComputedAt { get; set; }
}
Explicit event logging
await auditLog.LogAsync(
"pii.revealed",
entityType: typeof(Patient).FullName,
entityId: id.ToString()
);
Query audit entries
var entries = await readAuditLog.QueryAsync(
action: "entity.updated",
entityType: typeof(Patient).FullName,
limit: 50,
cancellationToken: cancellationToken
);
Configuration
PostgreSQL JSON columns
Pass jsonColumnType: "jsonb" to store OldValues, NewValues, and ChangedFields as native JSONB instead of serialized strings:
modelBuilder.ConfigureAuditLog(jsonColumnType: "jsonb");
Custom table and schema
modelBuilder.ConfigureAuditLog(tableName: "audit_entries", schema: "audit");
Sensitive data strategies
| Strategy | Behavior |
|---|---|
Redact (default) |
Replaces value with "***"; property name still appears in ChangedFields |
Exclude |
Omits the property entirely from OldValues, NewValues, and ChangedFields |
Transform |
Passes value through AuditLogOptions.SensitiveValueTransformer (hash, mask, tokenize) |
SensitiveValueTransformer must be configured whenever the effective strategy is Transform. Global misconfiguration fails options resolution; per-property [AuditSensitive(SensitiveDataStrategy.Transform)] without a transformer throws an OptionsValidationException during capture instead of silently redacting.
Per-property strategy override:
[AuditSensitive(SensitiveDataStrategy.Exclude)]
public string CreditCardToken { get; set; } = "";
Key Behaviors
- Atomicity - Audit entries are added to the same DbContext and committed in the same transaction as entity changes
- Zero overhead - When
IsEnabledisfalse,CaptureChangesreturns an empty list immediately - Disabled auditing signal - When
IsEnabledisfalse, the first capture attempt logs a warning with remediation guidance - Soft-delete detection - Monitors
IsDeletedandIsSuspendedproperty transitions; emits semantic action names instead of genericentity.updated - Owned entities - Inherit auditability from their aggregate owner
- Audit capture errors are non-fatal - If capturing a single entity fails, a warning is logged and the save continues without that entry
- JSON round-trip shape -
OldValuesandNewValuesdeserialize non-string values asJsonElement; useGetDecimal(),GetBoolean(), and similar APIs when reading them back - Composite key encoding - Single-column
EntityIdvalues remain plain strings; composite keys are serialized as JSON string arrays such as["tenant-a","order,42"] - Client metadata -
IpAddressandUserAgentare persisted when explicitly supplied, but automatic EF change capture does not populate them
SQLite Limitation
The default entity configuration uses a composite primary key (CreatedAt, Id) for partition-readiness. SQLite does not support ValueGeneratedOnAdd (autoincrement) on composite keys. Consumers targeting SQLite must override the key configuration — for example, using a single-column PK on Id:
builder.HasKey(e => e.Id); // Override for SQLite
Migration Note
Composite-key EntityId values are now serialized as JSON arrays instead of comma-joined strings. Existing stored audit rows using the old comma-joined format remain unchanged; downstream parsers should handle both shapes during transition.
Dependencies
Headless.AuditLog.AbstractionsHeadless.Orm.EntityFrameworkMicrosoft.EntityFrameworkCore
Side Effects
- Registers
IAuditChangeCaptureas scoped (EfAuditChangeCapture) - Registers
IAuditLogStoreas scoped (EfAuditLogStore) - Registers
IAuditLog<TContext>as scoped (EfAuditLog<TContext>) - Registers
IReadAuditLog<TContext>as scoped (EfReadAuditLog<TContext>)
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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
- CommunityToolkit.HighPerformance (>= 8.4.2)
- EFCore.CheckConstraints (>= 10.0.0)
- FluentValidation (>= 12.1.1)
- Headless.AuditLog.Abstractions (>= 0.4.15)
- Headless.Orm.EntityFramework (>= 0.4.15)
- Humanizer.Core (>= 3.0.10)
- IdGen (>= 3.0.7)
- JetBrains.Annotations (>= 2025.2.4)
- libphonenumber-csharp (>= 9.0.30)
- Microsoft.Bcl.TimeProvider (>= 10.0.8)
- Microsoft.EntityFrameworkCore (>= 10.0.8)
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.8)
- Microsoft.Extensions.Caching.Abstractions (>= 10.0.8)
- Microsoft.Extensions.Caching.Memory (>= 10.0.8)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.8)
- Microsoft.Extensions.DependencyInjection (>= 10.0.8)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.8)
- Microsoft.Extensions.Hosting (>= 10.0.8)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.8)
- Microsoft.Extensions.Logging (>= 10.0.8)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.8)
- Microsoft.Extensions.Logging.Console (>= 10.0.8)
- Microsoft.Extensions.Options (>= 10.0.8)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.8)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.8)
- morelinq (>= 4.4.0)
- Nito.AsyncEx (>= 5.1.2)
- Nito.Disposables (>= 2.5.0)
- Polly.Core (>= 8.6.6)
- Scrutor (>= 7.0.0)
- Snappier (>= 1.3.1)
- System.Reactive (>= 6.1.0)
- TimeZoneConverter (>= 7.2.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.