How-to guide
Audit data access
Record who changed what, redact sensitive values and route an audit trail to your own tooling from inside the data layer. Core provides the auditing hooks, and the Enterprise edition adds ready-made adapters.
Prerequisites
- A way to resolve the current user
- For the ready-made adapters, the Enterprise edition with the commercial auditing package referenced
Regulated workloads normally get a bespoke auditing layer bolted onto the data-access code after the fact. In Kiwi ORM the governance is built into the pipeline, so every query, in every mode, can be observed and governed without changing application code.
Auditing is split across the two editions:
| Free (Core) | Commercial (Enterprise) | |
|---|---|---|
Auditing hooks: the IAuditAdapter contract, UseAuditAdapter, the audit context the pipeline builds for each operation |
Yes | Yes |
| Your own audit code behind those hooks | Yes | Yes |
| Ready-made, tested, zero-code audit adapters for all 11 relational databases | No | Yes |
| Ready-made MongoDB audit adapter | No | Yes |
| Query audit trail, data access policies, compliance controls | No | Yes |
Note
Free in Core, ready-made in Enterprise. ORM Core gives you the framework and the hooks, and you write the audit code. The Enterprise edition adds fully tested adapters so you do not have to. See Editions and features.
What is recorded#
- A query audit trail: each operation with the user, timestamp, parameters and result metadata.
- Row-level change detail for inserts, updates, deletes and soft deletes: the table, the primary key, and for each column the old value, new value and data type.
- Raw SQL statements, with their source (such as raw execution, a generated command, a repository or a stored procedure), operation type, target table, parameters, rows affected, duration and any error.
- Context for each change: the application name, host name, IP address, transaction id and, optionally, a reason and a ticket number.
- Sensitive-value redaction: columns marked
[Sensitive]have their values replaced with***REDACTED***. The column name and the fact that it changed are still recorded.
Choose what is audited#
Mark entities with [Auditable]. The AuditType flag selects the operations (Insert, Update, Delete, or All, the default):
[Auditable(AuditType = AuditType.Update | AuditType.Delete)]
[TableInfo("customers")]
public class Customer : Entity<Customer>
{
[Sensitive]
[ColumnInfo("national_id")]
public string NationalId { get => _nationalId; set => SetAndNotify(ref _nationalId, value); }
private string _nationalId = "";
// ...
}
[Sensitive] is for passwords, national identification numbers, payment details, API keys and similar. The value is redacted, not omitted: the audit still shows that the column changed. Redaction works in both editions: the pipeline redacts before the audit context reaches an adapter, so a custom adapter never receives the real value either.
Say why a change was made#
Every insert, update and delete command can carry a justification and a link to an external ticket:
db.Update(customer)
.WithAuditReason("Annual address review, approved by the account owner")
.WithTicketNumber("CASE-1042")
.Execute();
Set RequireAuditReason = true on [TableInfo] to make the reason mandatory for a table. A modification without WithAuditReason then fails with an InvalidOperationException before it reaches the database.
Write your own audit adapter (free)#
ORM Core includes the auditing hooks. Implement IAuditAdapter and register it with UseAuditAdapter:
using Kiwify.Kiwi.ORM.Abstractions.Auditing;
public sealed class LogAuditAdapter : IAuditAdapter
{
public void Initialize() { }
public void SaveAudit(AuditContext audit)
{
// audit.Operation: Insert, Update, Delete or SoftDelete
Log.Information("{Op} {Table} by {User} at {At}: {Changes} columns changed",
audit.Operation, audit.TableName, audit.UserId, audit.TimestampUtc,
audit.ColumnChanges?.Count ?? 0);
}
public void SaveRawSqlAudit(RawSqlAuditContext audit)
{
Log.Information("{Source} {Type} by {User} took {Ms} ms",
audit.Source, audit.OperationType, audit.UserId, audit.DurationMs);
}
public void Dispose() { }
}
db.UseAuditAdapter<SalesContext>(new LogAuditAdapter());
The pipeline builds an AuditContext for each audited entity change and a RawSqlAuditContext for each raw SQL statement. What you do with them, such as writing to a table, a queue or a file, is yours to decide. Register the acting user once with RegisterCurrentUserResolver(() => currentUser.Name).
Use the ready-made adapters (Enterprise)#
Note
Commercial feature. This capability is part of the Kiwi ORM Enterprise edition. The source-available core does not include it. See Editions and features.
Reference the commercial auditing package for your audit store, then enable it on the context. The extension methods only appear when that package is referenced. The adapters are tested on all 11 supported relational databases, and a MongoDB adapter is also available.
using Kiwify.Kiwi.ORM.Auditing.Adapters.Rdbms;
using var db = DatabaseContext
.CreateContext<SalesContext>(i => i.UseSqlServer(connectionString))
.Connect()
.RegisterCurrentUserResolver(() => currentUser.Name) // who is acting
.EnableDefaultAuditing();
EnableDefaultAuditing() stores the audit trail in the same database, creating three tables unless you disable that:
| Table | Content |
|---|---|
Audit_Operations |
One row per audited operation |
Audit_Details |
The per-column changes of each operation |
Audit_RawSql |
Raw SQL statements |
Customise names with AuditTableConfig (AuditTableName, AuditDetailTableName, AuditRawSqlTableName, SchemaName, TablePrefix) and pass autoCreateTables: false if you create the tables yourself.
Choose where the audit trail is stored#
Audit data can live in your relational database or in MongoDB.
| Store | How |
|---|---|
| The application database | EnableDefaultAuditing() |
| A separate relational audit database | EnableRdbmsAuditing(auditDbContext) keeps audit rows out of the application database |
| Full control of the relational adapter | EnableRdbmsAuditing(new RdbmsAuditAdapterConfig { ... }), including an error handler, a logger and a custom table script provider |
| MongoDB | The MongoDB audit adapter, configured with a connection string, database name and the Audits and RawSqlAudits collections |
Note
MongoDB is supported as an audit store only. Kiwi ORM does not support it as a data source.
By default an audit failure is logged and does not break the operation being audited. For workloads where the audit is mandatory, set ThrowOnAuditFailure = true on the adapter configuration so that a failed audit write fails the operation.
Send audit records elsewhere#
The IAuditAdapter extension point lets you send audit records to SIEM, Splunk or other compliance tooling by implementing a custom adapter. This is available in both editions. Kiwi ORM does not ship a separate log-export feature.
Compliance#
The Enterprise controls are technical measures designed to support the requirements of SOC 2, GDPR, HIPAA and ISO 27001; they support, but do not on their own establish, compliance with any framework.
Data access policies#
The Enterprise edition also includes row-level and column-level access controls that can be enforced at the ORM layer, so restrictions apply to every mode instead of being re-implemented in each application. Ask Kiwify Labs for the current policy configuration reference for your release.
Design notes#
Auditing runs through the same pipeline as everything else, so enabling it does not mean adopting a second data-access layer.
Expected result
- Insert, update and delete operations, and raw SQL, are recorded with the acting user and redacted where marked sensitive