Skip to content

Event History Viewer (Backend)

Audience: developers implementing the backend query/handler. See also: Event History Viewer: How It Works for the end-user perspective, and Adding Event History to Pages for the frontend integration guide.

This document describes the event history viewing feature in Foundry, which allows viewing the complete event stream for any aggregate.

Overview

The event history viewer provides a way to: - View all events for a specific aggregate - Search and filter events - View detailed event data - Navigate between events - Download event history as JSON

This is useful for: - Debugging aggregate state issues - Auditing changes - Understanding the sequence of events - Training and documentation

Backend Implementation

Query

File: src/Domain/LBS.Domain.Infrastructure/Events/GetAggregateEventsQuery.cs

[RequiresRoles(RoleDefinition.Member)]
[DataContract(Name = "GetAggregateEvents", Namespace = LbsNamespace.CoreQueries)]
public record GetAggregateEventsQuery : ISecureQuery, IRequireUserContext
{
    [DataMember(Order = 1)]
    public required Guid AggregateRootId { get; init; }

    [DataMember(Order = 2)]
    public int? AfterVersion { get; init; }

    [IgnoreDataMember]
    public string QueryType => this.GetType().GetQueryTypeName();

    [IgnoreDataMember]
    public ClaimsPrincipal? User => UserContext.Current;
}

Query Handler

File: src/Domain/LBS.Domain.Infrastructure/Events/GetAggregateEventsQueryHandler.cs

The handler: 1. Fetches the event stream from Marten using IDocumentSession.Events.FetchStreamAsync() 2. Optionally filters events by version 3. Maps events to AggregateEventContract DTOs 4. Extracts user information from events using reflection

Contract

File: src/Domain/LBS.Domain.Infrastructure/Events/AggregateEventContract.cs

[DataContract]
public sealed class AggregateEventContract
{
    [DataMember(Order = 1)]
    public Guid EventId { get; init; }

    [DataMember(Order = 2)]
    public Guid AggregateRootId { get; init; }

    [DataMember(Order = 3)]
    public string EventType { get; init; } = string.Empty;

    [DataMember(Order = 4)]
    public int Version { get; init; }

    [DataMember(Order = 5)]
    public DateTimeOffset Timestamp { get; init; }

    [DataMember(Order = 6)]
    public string? UserId { get; init; }

    [DataMember(Order = 7)]
    public string? UserName { get; init; }

    [DataMember(Order = 8)]
    public object? EventData { get; init; }
}

Frontend Implementation

Query Helper

File: src/Apps/foundry-web/src/lib/utils/query-helpers.ts

export async function getAggregateEvents(
    aggregateRootId: string,
    options?: { afterVersion?: number }
): Promise<ReadModelResponse<AggregateEvent[]>> {
    const query: GetAggregateEvents = {
        queryType: 'GetAggregateEvents',
        aggregateRootId,
        afterVersion: options?.afterVersion ?? null
    };

    return await authStore.client.query<GetAggregateEvents, AggregateEvent[]>(query);
}

Components

File: src/Apps/foundry-web/src/lib/components/events/event-history-side-panel.svelte

Side panel overlay component features: - Slides in from the right side of the screen - Backdrop overlay dims the main content - Click outside or X button to close - Loads and displays event list for an aggregate - Real-time search/filter functionality - Download as JSON - Refresh button - Styled event cards with metadata - Click to view full event details - Responsive width (full width on mobile, 600-700px on desktop)

Props: - aggregateRootId: string - The aggregate ID to load events for - title?: string - Panel title (default: "Event History") - open: boolean - Bindable prop to control panel visibility

EventHistoryPanel

File: src/Apps/foundry-web/src/lib/components/events/event-history-panel.svelte

Embedded card component (for use in tabs or full pages): - Loads and displays event list for an aggregate - Real-time search/filter functionality - Download as JSON - Refresh button - Styled event cards with metadata - Click to view full event details

EventDetailsDialog

File: src/Apps/foundry-web/src/lib/components/events/event-details-dialog.svelte

Dialog features: - Shows full event details - Navigate between events (previous/next) - Copy event/aggregate IDs to clipboard - Smart value rendering (dates, GUIDs, objects, arrays) - Raw JSON view - Filters out system properties

Usage

Standalone Page

A debug page is available at /debug/events/[id] where [id] is the aggregate root ID.

File: src/Apps/foundry-web/src/routes/debug/events/[id]/+page.svelte

<script lang="ts">
    import EventHistoryPanel from '$lib/components/events/event-history-panel.svelte';

    const aggregateId = $derived($page.params.id!);
</script>

<EventHistoryPanel aggregateRootId={aggregateId} title="Event History" />

The recommended approach is to add an "Event History" button that opens a side panel. This pattern is already implemented in the competitions detail page.

Example: src/Apps/foundry-web/src/routes/competitions/[id]/+page.svelte

<script lang="ts">
    import EventHistorySidePanel from '$lib/components/events/event-history-side-panel.svelte';
    import { History } from '@lucide/svelte';

    let competition: Competition | null = $state(null);
    let showEventHistory = $state(false);
</script>

<!-- Add button in page header -->
<Button variant="outline" size="sm" onclick={() => showEventHistory = true} disabled={!competition}>
    <History class="mr-2 h-4 w-4" />
    Event History
</Button>

<!-- Add side panel at end of page -->
{#if competition}
    <EventHistorySidePanel
        aggregateRootId={competition.id}
        title="Competition Event History"
        bind:open={showEventHistory}
    />
{/if}

Tabs Pattern (Alternative)

For pages where you want event history as a permanent tab:

<script lang="ts">
    import EventHistoryPanel from '$lib/components/events/event-history-panel.svelte';
    import * as Tabs from '$lib/components/ui/tabs';

    let participant: Participant | null = $state(null);
</script>

<Tabs.Root value="details">
    <Tabs.List>
        <Tabs.Trigger value="details">Details</Tabs.Trigger>
        <Tabs.Trigger value="events">Event History</Tabs.Trigger>
    </Tabs.List>

    <Tabs.Content value="details">
        <!-- Existing detail view -->
    </Tabs.Content>

    <Tabs.Content value="events">
        {#if participant?.id}
            <EventHistoryPanel
                aggregateRootId={participant.id}
                title="Participant Events"
            />
        {/if}
    </Tabs.Content>
</Tabs.Root>

Programmatic Usage

import { getAggregateEvents } from '$lib/utils/query-helpers';

// Get all events
const response = await getAggregateEvents(aggregateId);
console.log(response.data); // Array of AggregateEvent

// Get events after version 5
const newEvents = await getAggregateEvents(aggregateId, { afterVersion: 5 });

Security

  • Query requires Member role (via [RequiresRoles(RoleDefinition.Member)])
  • Uses authenticated SDK client
  • Only members can view event history
  • User information is extracted from events when available

Example Use Cases

1. Debugging State Issues

When an aggregate is in an unexpected state: 1. Navigate to /debug/events/{aggregateId} 2. Review the event sequence 3. Identify which event caused the issue 4. Check event data for incorrect values

2. Auditing Changes

To see who made changes and when: 1. Open event history for the aggregate 2. Filter by user name 3. Review timestamps and event types 4. Download as JSON for records

3. Understanding Event Flow

For training or documentation: 1. Perform actions on an aggregate 2. View the generated events 3. See how commands translate to events 4. Understand aggregate behavior

Performance Considerations

  • Events are loaded once on mount
  • Search/filter happens client-side
  • Large event streams (1000+) may be slow to render
  • Consider pagination for very large streams (future enhancement)

Future Enhancements

Potential improvements: - [ ] Pagination for large event streams - [ ] Event timeline visualization - [ ] Replay to specific version - [ ] Compare two event versions - [ ] Export filtered events - [ ] Real-time event subscription - [ ] Event type filtering - [ ] Date range filtering