Skip to content

Audit Trail

The Audit Trail module provides opt-in audit logging for dispatch handlers and a standard interface for querying audit entries.

Quick Start

using Deepstaging.Dispatch;
using Deepstaging.Effects;
using Deepstaging.Audit;

public static class OrderCommands
{
    [CommandHandler(Audited = true)]
    public static Eff<AppRuntime, OrderUpdated> Handle(UpdateOrder cmd) => ...
}

[EffectsModule(typeof(IAuditStore))]
public sealed partial class AuditEffects;

When Audited = true, the dispatch pipeline records an AuditEntry with actor, action, entity snapshots (before/after), metadata, and timestamp.

Pipeline order: authorize → validate → handler → auto-commit → audit.

Interface

Method Returns Description
SaveAsync(entry) Task Record an audit entry
GetByEntityAsync(entityType, entityId, offset?, limit?) Task<IReadOnlyList<AuditEntry>> Query by entity
GetByActorAsync(actor, offset?, limit?) Task<IReadOnlyList<AuditEntry>> Query by actor
GetByActionAsync(action, from, to, offset?, limit?) Task<IReadOnlyList<AuditEntry>> Query by action in time range

Usage

// Query audit history for an entity
from entries in AuditEffects.AuditStore.GetByEntityAsync<AppRuntime>(
    "Order", orderId, offset: 0, limit: 50)
select entries;

Templating

Use Scriban templates to format human-readable audit trail messages from structured AuditEntry data. Templates keep audit message formatting consistent and centralized.

{{- # Templates/Audit/EntityChanged.scriban-txt -}}
{{ actor }} {{ action | string.downcase }} {{ entity_type }} {{ entity_id }}
{{- if changes }}:
{{ for change in changes }}
  {{ change.field }}: {{ change.old_value }} → {{ change.new_value }}
{{ end }}
{{- end }}

This produces messages like: admin@example.com updated Order ORD-123: Status: Pending → Shipped

For syntax reference and best practices, see Scriban Templating.

Automatic vs. manual audit

Set Audited = true on dispatch handlers for automatic audit trail. For manual entries outside the dispatch pipeline, call AuditEffects.AuditStore.SaveAsync directly.