Themia.Modules.Audit 0.25.1

dotnet add package Themia.Modules.Audit --version 0.25.1
                    
NuGet\Install-Package Themia.Modules.Audit -Version 0.25.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Themia.Modules.Audit" Version="0.25.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Themia.Modules.Audit" Version="0.25.1" />
                    
Directory.Packages.props
<PackageReference Include="Themia.Modules.Audit" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Themia.Modules.Audit --version 0.25.1
                    
#r "nuget: Themia.Modules.Audit, 0.25.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Themia.Modules.Audit@0.25.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Themia.Modules.Audit&version=0.25.1
                    
Install as a Cake Addin
#tool nuget:?package=Themia.Modules.Audit&version=0.25.1
                    
Install as a Cake Tool

Themia.Modules.Audit

The framework half of Themia's audit log: tenant resolution, transaction enlistment, the IAuditLogService adapter, and an observer that audits every Identity authentication event without the application writing any code.

net10.0. Builds on Themia.Audit, which owns the record model, redaction, the store and the schema.

Install

services.AddThemiaAudit(o =>                     // Themia.Audit — store, redaction, schema
{
    o.ConnectionString = cfg.GetConnectionString("Default")!;
    o.Engine           = AuditEngine.Postgres;
});
services.AddThemiaAuditPostgreSql();

services.AddThemiaAuditModule();                 // this package
services.AddThemiaAuditIdentityObserver();       // opt-in: audit Identity

Call order between AddThemiaAudit and AddThemiaAuditModule does not matter — this package supersedes the neutral recorder by identity rather than by which call ran last, and that is asserted by tests rather than assumed.

AddThemiaAuditIdentityObserver is a separate call on purpose. A host without Identity is never made to reference it, and a host that has Identity but does not want authentication auditing should not have to opt out of something that turned itself on.

Two write surfaces

IAuditRecorder is the real API. IAuditLogService (from Themia.Services) is a compatibility adapter over it, and its AuditEvent record cannot express an outcome, a reason, an IP address, or a null actor — so it cannot describe a failed login. Prefer IAuditRecorder.

Transaction policy

An audit row recording a change that was rolled back is worse during an investigation than a missing row: it asserts something that did not happen.

category policy why
Activity RequireTransaction (default, configurable) the guarantee is real, or its absence is loud
Authentication, UserLifecycle Never, not configurable a failed login has no transaction to join, and must be recorded even when the surrounding request fails

Under RequireTransaction with no open transaction, recording throws and the message names IUnitOfWork.ExecuteInTransactionAsync. That is not a Themia limitation being surfaced — a business write and its audit row cannot be atomic without a transaction on any database. Making the caller say so is the honest form.

await unitOfWork.ExecuteInTransactionAsync(async ct =>
{
    await proposals.AcceptAsync(id, ct);
    await recorder.RecordAsync(new AuditEntry { /* … */ }, payload, ct);
});

AuditTransactionPolicy.JoinIfPresent is available for adopters who accept the weaker guarantee. It degrades to durable-only when no transaction is open — stated here because that degradation is otherwise invisible.

Works identically on EF Core and Dapper; both legs are covered by the same tests.

The audit table must live in the adopter's own database

Under RequireTransaction — the default for activity events — the row is written on the caller's connection, against the unqualified table name themia_audit_events. AuditOptions.ConnectionString is used only by the migration, the dashboard, ITenantAuditReader and the Never policy.

Pointing ConnectionString at a separate audit database therefore creates the table there while activity writes go to the application's database and fail with a missing-table error on the first attempt. Nothing detects the mismatch at startup — the two connections are opened by different components at different times, and a connection string does not say which database a unit of work will later use.

A separate audit database works only with AuditTransactionPolicy.Never for every category, which trades away the atomicity guarantee this module exists to provide: an activity row then survives a rolled-back business write and records something that did not happen.

Reading

ITenantAuditReader pre-seeds the ambient tenant and overrides an AuditQuery that names a different one — it cannot be asked for another tenant's rows.

The raw IAuditStore.QueryAsync crosses tenants by design, because the neutral package cannot see ITenantContext. Adopter code should use ITenantAuditReader.

What auditing Identity gives you

AddThemiaAuditIdentityObserver records all twelve events Identity raises, with no changes at your call sites:

event notable
LOGIN_SUCCEEDED / LOGIN_FAILED / LOGIN_DENIED Reason carries the real LoginFailureReason the uniform 401 hides from the client
EXTERNAL_LOGIN_SUCCEEDED / _FAILED / _DENIED the payload carries wasCreated and wasLinked — account auto-creation and provider linking are both takeover vectors
REFRESH_SUCCEEDED / REFRESH_DENIED / REFRESH_FAILED REFRESH_DENIED carries rotationCommitted, distinguishing "nothing happened" from "the token was rotated and its holder does not know"
LOCKED_OUT fires where lockout is applied, not inferred from the next attempt
LOGOUT, USER_MUTATED

REFRESH_FAILED with RefreshOutcome.ReuseDetected is the one to alert on: a rotated refresh token presented twice is the textbook signature of token theft, and the row is attributed to the owning account so an operator can revoke its sessions.

An observer that throws never changes the flow it observes — it is logged at Error and swallowed. A failed audit write must not turn a successful login into a 500, and must not turn a failed login into a different status code, which would leak the reason the uniform 401 exists to hide.

Schema

AddThemiaAudit runs the migration. AuditModule asserts the table exists and does not migrate.

  • Themia.Audit — record model, redaction, store, dialects, schema.
  • Themia.Audit.AspNetCore — mountable read-only dashboard. Its Authorize predicate is fail-closed and tenant scoping there is the adopter's decision, since a host admin viewing every tenant and a tenant admin viewing their own are both legitimate.
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.25.1 39 9/11/2026
0.25.0 43 9/10/2026
0.24.0 50 9/10/2026
0.23.1 58 9/7/2026
0.23.0 68 9/7/2026