Skip to content

Temporal Perspectives

Verified by tests

ITemporalPerspectiveForTests, ITemporalPerspectiveStoreTests, TemporalActionTypeTests, TemporalPerspectiveRowTests, ITemporalLensQueryTests — library CI run #31657041675 (2026-08-13)

Temporal perspectives create append-only logs where each event creates a NEW row rather than updating existing rows. This pattern is ideal for activity feeds, audit logs, and full history tracking.

Updated

At commit 1b31f58d, temporal perspectives are contract-only: the interfaces on this page (ITemporalPerspectiveFor, ITemporalPerspectiveStore, ITemporalLensQuery, TemporalPerspectiveRow) ship in Whizbang.Core and are covered by unit tests, but no storage provider implements ITemporalPerspectiveStore or ITemporalLensQuery yet, and the source generators do not yet discover ITemporalPerspectiveFor implementations. End-to-end temporal processing (automatic INSERT of temporal rows and the query examples below) requires a provider implementation that has not shipped.

Overview

Pattern Interface Storage Use Case
Standard IPerspectiveFor UPSERT (one row per stream) Current state views
Temporal ITemporalPerspectiveFor INSERT (new row per event) Activity feeds, audit logs

Defining a Temporal Perspective

Defining a Temporal Perspective

public class ActivityPerspective :
    ITemporalPerspectiveFor<ActivityEntry, OrderCreatedEvent, OrderUpdatedEvent> {

  public ActivityEntry? Transform(OrderCreatedEvent @event) {
    return new ActivityEntry {
      SubjectId = @event.OrderId,
      Action = "created",
      Description = $"Order created for ${@event.TotalAmount}"
    };
  }

  public ActivityEntry? Transform(OrderUpdatedEvent @event) {
    return new ActivityEntry {
      SubjectId = @event.OrderId,
      Action = "updated",
      Description = $"Order status changed to {@event.NewStatus}"
    };
  }
}

Key Differences from IPerspectiveFor

  1. Transform vs Apply: Transform(event) instead of Apply(currentData, event)
  2. No current state: Transform only receives the event, not existing data
  3. Nullable return: Return null to skip an event (no entry created)
  4. Always INSERT: Never updates existing rows

Temporal Row Structure

Each temporal row includes:

Temporal Row Structure

public class TemporalPerspectiveRow<TModel> {
  public Guid Id { get; }           // UUIDv7 for time-ordering
  public Guid StreamId { get; }     // Aggregate ID
  public Guid EventId { get; }      // Source event ID
  public TModel Data { get; }       // Transformed entry
  public PerspectiveMetadata Metadata { get; }
  public PerspectiveScope Scope { get; }

  // Temporal tracking (SQL Server patterns)
  public TemporalActionType ActionType { get; }  // Insert/Update/Delete
  public DateTime PeriodStart { get; }           // When recorded (system time)
  public DateTime PeriodEnd { get; }             // When superseded
  public DateTimeOffset ValidTime { get; }       // Business time from event
}

Querying Temporal Data

All History

All History

var allHistory = await temporalLens
    .TemporalAll()
    .Where(r => r.StreamId == orderId)
    .OrderBy(r => r.PeriodStart)
    .ToListAsync();

Latest Per Stream

Latest Per Stream

var latestStates = await temporalLens
    .LatestPerStream()
    .ToListAsync();

Point-in-Time Query (As Of)

Point-in-Time Query (As Of)

var stateLastWeek = await temporalLens
    .TemporalAsOf(DateTimeOffset.UtcNow.AddDays(-7))
    .ToListAsync();

Time Range Queries

Time Range Queries

// Rows active during a range
var activeRows = await temporalLens
    .TemporalFromTo(startTime, endTime)
    .ToListAsync();

// Rows fully contained in a range
var containedRows = await temporalLens
    .TemporalContainedIn(startTime, endTime)
    .ToListAsync();

Convenience Methods

Convenience Methods

// Recent activity for a stream
var orderActivity = await temporalLens
    .RecentActivityForStream(orderId, limit: 20)
    .ToListAsync();

// Recent activity for a user
var userActivity = await temporalLens
    .RecentActivityForUser(userId, limit: 50)
    .ToListAsync();

Action Types

The TemporalActionType enum tracks what happened to the entity. Each temporal row includes an ActionType that indicates the kind of change:

Action Types

public enum TemporalActionType {
  Insert = 0,   // New entity was created (first entry in temporal history)
  Update = 1,   // Existing entity was modified
  Delete = 2    // Entity was soft-deleted or removed
}
Value Name Description
0 Insert New entity was created. This is the first entry in the temporal history for a stream.
1 Update Existing entity was modified. The entity already existed and its state has changed.
2 Delete Entity was soft-deleted or removed. The entity still exists in history but is no longer active.

Filtering Events

Return null from Transform to skip events:

Filtering Events

public ActivityEntry? Transform(OrderCreatedEvent @event) {
  // Only log high-value orders
  if (@event.TotalAmount < 100) {
    return null;  // Skip this event
  }

  return new ActivityEntry { ... };
}

Bi-Temporal Support

Temporal perspectives support both system time and business time:

  • PeriodStart/PeriodEnd: When the database recorded the change (system time)
  • ValidTime: When the event occurred in business terms

This enables queries like "what did we know about this order on January 15th?"

See Also

  • Standard Perspectives
  • ITemporalLensQuery Reference