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¶
- Transform vs Apply:
Transform(event)instead ofApply(currentData, event) - No current state: Transform only receives the event, not existing data
- Nullable return: Return
nullto skip an event (no entry created) - 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
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