Themia.Audit
0.25.1
dotnet add package Themia.Audit --version 0.25.1
NuGet\Install-Package Themia.Audit -Version 0.25.1
<PackageReference Include="Themia.Audit" Version="0.25.1" />
<PackageVersion Include="Themia.Audit" Version="0.25.1" />
<PackageReference Include="Themia.Audit" />
paket add Themia.Audit --version 0.25.1
#r "nuget: Themia.Audit, 0.25.1"
#:package Themia.Audit@0.25.1
#addin nuget:?package=Themia.Audit&version=0.25.1
#tool nuget:?package=Themia.Audit&version=0.25.1
Themia.Audit
Framework-neutral, append-only audit event log: a record model with validation and unconditional redaction, a Dapper store behind a per-engine dialect strategy, and the FluentMigrator schema.
net8.0;net10.0. No Themia.Framework.* dependency — asserted by a test, not left to convention.
What this package is for
Two kinds of event share one table and one write path:
- Activity — business events an application chooses to record.
- Authentication / user lifecycle — login, logout, refresh, lockout and credential changes,
recorded for the application by
Themia.Modules.Auditwithout it writing any code.
An entity change log — per-row CREATE/UPDATE/DELETE with field-level old→new values — is a
different shape and is not in this package. It is planned for 0.24.0.
Install
dotnet add package Themia.Audit
dotnet add package Themia.Audit.PostgreSql # or .SqlServer / .MySql
services.AddThemiaAudit(o =>
{
o.ConnectionString = cfg.GetConnectionString("Default")!;
o.Engine = AuditEngine.Postgres;
});
services.AddThemiaAuditPostgreSql();
AddThemiaAudit runs the schema migration by default, so this package works without the module
layer. Pass runMigration: false to defer it. Calling it with runMigration: true and an unset
Engine or empty ConnectionString throws immediately and names the missing setting — a requested
migration that silently does not run would surface much later as a missing-table error at the first
write.
The migration needs both calls. AddThemiaAudit records the request; the matching
AddThemiaAudit{Engine}() supplies the engine, and whichever of the two runs second applies the
migration. Order between them does not matter, but both are required: if you use this core with your
own IAuditDialect/IAuditStore and never call one of AddThemiaAuditPostgreSql() /
AddThemiaAuditMySql() / AddThemiaAuditSqlServer(), the requested migration cannot run, and startup
fails with an options-validation error naming the call to add. Pass runMigration: false if you create
the audit table yourself.
Recording an event
await recorder.RecordAsync(
new AuditEntry
{
EventType = "PROPOSAL_ACCEPTED",
Category = AuditCategory.Activity,
Outcome = AuditOutcome.Success,
ActorId = currentUser.Id,
EntityType = nameof(Proposal),
EntityId = proposal.Id.ToString(),
},
payload: new { proposal.Status, proposal.Amount });
You pass an object, never a JSON string. The recorder serializes it and redacts it, so data is
well-formed JSON by construction and the redactor always has something it can walk. AuditEntry.Data
is internal init precisely so this cannot be bypassed.
Redaction
Unconditional, on the single write path — not an option and not a helper you can forget. Property
names matching the deny-list have their values replaced with [redacted] at any depth, including
inside nested objects and arrays. The property name is kept: that a password field was present is
itself audit-relevant.
password, passwordhash, passwordsalt, secret, token, refreshtoken, accesstoken,
apikey, api_key, authorization, otp, pin, cvv, creditcard, card_number, privatekey, ssn
AuditRedactionOptions.AddPattern adds to that list and cannot remove from it. Use
IsSensitive(name) to test membership; there is deliberately no public collection to enumerate,
because no shape of one was safe to expose.
An adopter who genuinely needs a default gone supplies their own IAuditRedactor — a conspicuous act
rather than a config line.
Storage
One table, themia_audit_events, unqualified and identical on every engine. Never
InSchema(...): FluentMigrator drops it on MySQL, where "schema" and "database" are the same concept,
so a qualified name means something different per engine — which is how two Themia modules once ended
up with colliding table names there.
data is nvarchar(max)/text on every engine, never jsonb or JSON. Those types validate on
insert, and an adopter-supplied payload must never be able to fail the transaction it describes on
some engines but not others.
id is a bigint identity clustered key; event_uid is the public Guid. A random Guid clustered
key page-splits an append-only table on every insert.
Bounded columns have constants on AuditEntry (MaxEventTypeLength and siblings) bound to the
migration. Fields an adopter names — EventType, ActorId, EntityType, EntityId — are
rejected when over-length rather than truncated, because truncating them merges two distinct
events into one. Fields the framework captures — UserAgent, IpAddress — are truncated, because a
clipped user-agent is still the same event.
Reads: the query surface lives here, not in the module
IAuditStore.QueryAsync is defined and implemented in Themia.Audit (this package) — not in
Themia.Modules.Audit. You can query without taking the module at all: construct an AuditQuery
and call QueryAsync against any IAuditStore your engine package registers.
QueryAsync applies no tenant predicate unless AuditQuery asks for one, because this package is
framework-neutral and cannot see ITenantContext. AuditQuery has two independent knobs for this,
not one:
AuditQuery.TenantId = null(the default) means no filter — rows for every tenant and every host-level row (see below) come back. This is not "tenant-locked"; it is the widest read the store can do.AuditQuery.TenantId = "<id>"narrows to that one tenant's rows only.AuditQuery.HostLevelOnly = truenarrows to rows where the storedAuditEntry.TenantId IS NULL— the single-org case, where every row belongs to "the org" and there is no per-tenant filter to apply in the first place.
If you are multi-tenant and want the ambient tenant applied for you, ITenantAuditReader from
Themia.Modules.Audit sets AuditQuery.TenantId to the caller's tenant and refuses to be asked for
another tenant's rows. That module wrapper is a convenience over QueryAsync, not a gate in front of
it — nothing about Themia.Audit on its own restricts which tenant's rows a caller can read.
Tenant semantics
On the written row, AuditEntry.TenantId is nullable and null means host-level, not
"unknown". A failed login for an identifier matching no user genuinely has no tenant, and is exactly
the row a security review wants. Nothing in this package invents a tenant.
This is the same nullable TenantId shape as the query filter above, but a different property on a
different type: AuditEntry.TenantId is what got recorded; AuditQuery.TenantId (and
HostLevelOnly) is how you filter it back out.
Retention
IAuditStore.PurgeAsync(olderThan, …) deletes in one statement. There is no scheduled job: the
default is keep-forever, and silently deleting an audit trail is worse than a growing table. On a
table that has never been purged the first call can be very large — purge in date slices rather than
in one go.
Related
Themia.Audit.AspNetCore— mountable read-only dashboard.Themia.Modules.Audit— tenant resolution, transaction enlistment, and automatic auditing of Identity.
| 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 was computed. 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
- Dapper (>= 2.1.79)
- FluentMigrator (>= 8.0.1)
- FluentMigrator.Runner.Core (>= 8.0.1)
- Themia.Data.Migrations (>= 0.25.1)
-
net8.0
- Dapper (>= 2.1.79)
- FluentMigrator (>= 8.0.1)
- FluentMigrator.Runner.Core (>= 8.0.1)
- Microsoft.Extensions.DependencyInjection (>= 10.0.9)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Options (>= 10.0.9)
- Themia.Data.Migrations (>= 0.25.1)
NuGet packages (5)
Showing the top 5 NuGet packages that depend on Themia.Audit:
| Package | Downloads |
|---|---|
|
Themia.Audit.AspNetCore
Themia.Audit ASP.NET Core dashboard — a mountable, self-rendered, read-only /audit UI (list + detail) over IAuditStore, with a fail-closed Authorize hook. |
|
|
Themia.Audit.SqlServer
SQL Server dialect (Microsoft.Data.SqlClient) for Themia.Audit. |
|
|
Themia.Audit.MySql
MySQL dialect (MySqlConnector) for Themia.Audit. |
|
|
Themia.Audit.PostgreSql
PostgreSQL dialect (Npgsql) for Themia.Audit. |
|
|
Themia.Modules.Audit
Framework-side audit module: transaction enlistment over Themia.Audit, ambient-tenant resolution, a tenant-scoped read surface, and the IAuditLogService adapter. |
GitHub repositories
This package is not used by any popular GitHub repositories.