---
title: Event Append Collection
---

import { Tabs, TabItem } from '@astrojs/starlight/components';

`IEventAppendCollection` is a scoped collector that captures every event appended to the event log
while it is active. It is the primary tool for asserting on appended events in integration tests.

## How It Works

Calling `StartCollectingAppends()` on the fixture subscribes a new `IEventAppendCollection` to the
`AppendOperations` observable on the event log. Every subsequent call to `EventStore.EventLog.Append`
or `EventStore.EventLog.AppendMany` is captured automatically and stored in the collection as an
`AppendedEventWithResult`. This includes events appended by reactors that use
`ICommandPipeline.Execute()` — because the command pipeline commits its unit of work through the
same `AppendMany` path, those appends are captured just like direct ones.

Because `Append` is awaited, the append operation is complete and the event is recorded in the
collection by the time the `await` returns. When testing reactors that append follow-up events
asynchronously, use `WaitForCount()` to wait for the expected number of events to arrive.

## One-Time Test Project Setup

`IEventAppendCollection` and `ChronicleInProcessFixture` come from the `Cratis.Chronicle.XUnit.Integration`
package. Every test project using them needs two small, one-time pieces of boilerplate: an xUnit
collection definition so every test class in the project shares one `ChronicleInProcessFixture`
(starting the underlying MongoDB container once, not per test class), and a project-local
`Specification` convenience base that closes the generic `Specification<TChronicleFixture>` over
your fixture type:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
namespace Cratis.Chronicle.Docs.EventAppendCollection
{
    // One-time test project setup: a collection definition sharing one ChronicleInProcessFixture
    // across test classes, and a project-local Specification convenience base.
    [CollectionDefinition(Name)]
    public class ChronicleCollection : ICollectionFixture<ChronicleInProcessFixture>
    {
        public const string Name = "Chronicle";
    }

    public class Specification(ChronicleInProcessFixture fixture) : Specification<ChronicleInProcessFixture>(fixture)
    {
        public override bool AutoDiscoverArtifacts => false;
    }

    [EventType]
    public record ItemRegistered(string Name);

    namespace given
    {
        public class an_event_append_collection_scope(ChronicleInProcessFixture fixture) : Specification(fixture)
        {
            public EventSourceId EventSourceId = null!;
            public IEventAppendCollection AppendedEventsCollector = null!;

            public override IEnumerable<Type> EventTypes => [typeof(ItemRegistered)];

            void Establish() => EventSourceId = EventSourceId.New();

            async Task Because()
            {
                AppendedEventsCollector = StartCollectingAppends();
                await EventStore.EventLog.Append(EventSourceId, new ItemRegistered("Widget"));
            }

            void Destroy() => AppendedEventsCollector?.Dispose();
        }
    }
}
```

</TabItem>
</Tabs>

Every other example on this page builds on this same setup.

## Scope Lifetime

Create a scope immediately before the operation under test so no earlier events are captured. Dispose
it when the operation is done. The recommended pattern, on a `given` context class:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
namespace Cratis.Chronicle.Docs.EventAppendCollection
{
    // One-time test project setup: a collection definition sharing one ChronicleInProcessFixture
    // across test classes, and a project-local Specification convenience base.
    [CollectionDefinition(Name)]
    public class ChronicleCollection : ICollectionFixture<ChronicleInProcessFixture>
    {
        public const string Name = "Chronicle";
    }

    public class Specification(ChronicleInProcessFixture fixture) : Specification<ChronicleInProcessFixture>(fixture)
    {
        public override bool AutoDiscoverArtifacts => false;
    }

    [EventType]
    public record ItemRegistered(string Name);

    namespace given
    {
        public class an_event_append_collection_scope(ChronicleInProcessFixture fixture) : Specification(fixture)
        {
            public EventSourceId EventSourceId = null!;
            public IEventAppendCollection AppendedEventsCollector = null!;

            public override IEnumerable<Type> EventTypes => [typeof(ItemRegistered)];

            void Establish() => EventSourceId = EventSourceId.New();

            async Task Because()
            {
                AppendedEventsCollector = StartCollectingAppends();
                await EventStore.EventLog.Append(EventSourceId, new ItemRegistered("Widget"));
            }

            void Destroy() => AppendedEventsCollector?.Dispose();
        }
    }
}
```

</TabItem>
</Tabs>

Disposal unsubscribes the collection immediately. Any appends that occur after disposal are not
captured, which prevents tests sharing a fixture from interfering with each other.

## AppendedEventWithResult Members

Each entry in `All` is an `AppendedEventWithResult` record that pairs the appended event with the
full outcome of the operation:

| Member | Type | Description |
|---|---|---|
| `Event` | `AppendedEvent` | The appended event, including its context and deserialized content |
| `Event.Content` | `object` | The deserialized event object (cast to your event type for assertions) |
| `Event.Context` | `EventContext` | Metadata: event source, sequence number, correlation ID, causation chain, etc. |
| `Event.Context.EventSourceId` | `EventSourceId` | The event source the event was appended for |
| `Event.Context.SequenceNumber` | `EventSequenceNumber` | Assigned sequence number |
| `Event.Context.CorrelationId` | `CorrelationId` | Correlation ID active at the time of the append |
| `Event.Context.Causation` | `IEnumerable<Causation>` | The causation chain active at the time of the append |
| `Result` | `AppendResult` | The outcome of the append operation |
| `Result.IsSuccess` | `bool` | `true` when the sequence number is valid and there are no violations or errors |
| `Result.SequenceNumber` | `EventSequenceNumber` | Assigned sequence number; `EventSequenceNumber.Unavailable` when the append failed |
| `Result.HasConstraintViolations` | `bool` | `true` when at least one constraint violation was returned |
| `Result.HasConcurrencyViolations` | `bool` | `true` when at least one concurrency violation was returned |
| `Result.HasErrors` | `bool` | `true` when at least one error was returned |
| `Result.ConstraintViolations` | `IEnumerable<ConstraintViolation>` | Constraint violations, if any |
| `Result.ConcurrencyViolation` | `ConcurrencyViolation?` | Concurrency violation, if any |
| `Result.Errors` | `IEnumerable<AppendError>` | Errors, if any |

## Asserting on Collected Events

`IEventAppendCollection` exposes three members:

| Member | Description |
|---|---|
| `All` | A snapshot of every `AppendedEventWithResult` captured so far |
| `Last` | The most recently captured `AppendedEventWithResult`; throws when nothing has been collected |
| `WaitForCount(count, timeout?)` | Waits asynchronously until at least `count` events have been collected |

### Single event

Pairing the `given` context above with a `Given<TSetup>`-derived spec class gives you access to the
collected events through `Context`:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
namespace Cratis.Chronicle.Docs.EventAppendCollection
{
    [Collection(ChronicleCollection.Name)]
    public class and_collecting_the_registered_item(given.an_event_append_collection_scope context)
        : Given<given.an_event_append_collection_scope>(context)
    {
        [Fact] void should_collect_one_event() => Context.AppendedEventsCollector.All.Count.ShouldEqual(1);
        [Fact] void should_have_appended_the_event() => Context.AppendedEventsCollector.All[0].Event.Content.ShouldBeOfExactType<ItemRegistered>();
        [Fact] void should_be_successful() => Context.AppendedEventsCollector.All[0].Result.IsSuccess.ShouldBeTrue();
        [Fact] void should_have_a_valid_sequence_number() => Context.AppendedEventsCollector.All[0].Result.SequenceNumber.IsActualValue.ShouldBeTrue();
    }
}
```

</TabItem>
</Tabs>

### Multiple events

When several events land in the same collection scope, use LINQ to locate the one you want:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
namespace Cratis.Chronicle.Docs.EventAppendCollection
{
    [EventType]
    public record FirstItemAdded(string Name);

    [EventType]
    public record SecondItemAdded(string Name);

    namespace given
    {
        public class a_multiple_events_scope(ChronicleInProcessFixture fixture) : Specification(fixture)
        {
            public EventSourceId EventSourceId = null!;
            public IEventAppendCollection AppendedEventsCollector = null!;

            public override IEnumerable<Type> EventTypes => [typeof(FirstItemAdded), typeof(SecondItemAdded)];

            void Establish() => EventSourceId = EventSourceId.New();

            async Task Because()
            {
                AppendedEventsCollector = StartCollectingAppends();
                await EventStore.EventLog.Append(EventSourceId, new FirstItemAdded("Widget"));
                await EventStore.EventLog.Append(EventSourceId, new SecondItemAdded("Gadget"));
            }

            void Destroy() => AppendedEventsCollector?.Dispose();
        }
    }

    [Collection(ChronicleCollection.Name)]
    public class and_locating_the_second_event(given.a_multiple_events_scope context)
        : Given<given.a_multiple_events_scope>(context)
    {
        AppendedEventWithResult SecondEvent => Context.AppendedEventsCollector.All.First(e => e.Event.Content is SecondItemAdded);

        [Fact] void should_locate_the_second_event() => SecondEvent.Event.Content.ShouldBeOfExactType<SecondItemAdded>();
        [Fact] void should_carry_the_correct_name() => ((SecondItemAdded)SecondEvent.Event.Content).Name.ShouldEqual("Gadget");
    }
}
```

</TabItem>
</Tabs>

## Waiting for Asynchronous Appends

When a reactor appends events after handling an incoming event, those appends happen asynchronously
on the server. Use `WaitForCount()` to wait for the expected number of events to arrive before
asserting. The events and reactor:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
namespace Cratis.Chronicle.Docs.EventAppendCollection
{
    [EventType]
    public record OrderPlaced(string OrderId);

    [EventType]
    public record ShipmentScheduled(string OrderId);

    public class ShipmentReactor(IEventLog eventLog) : IReactor
    {
        public Task OnOrderPlaced(OrderPlaced evt, EventContext ctx) =>
            eventLog.Append(ctx.EventSourceId, new ShipmentScheduled(evt.OrderId));
    }
}
```

</TabItem>
</Tabs>

The shared `given` context:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
namespace Cratis.Chronicle.Docs.EventAppendCollection
{
    namespace given
    {
        public class a_shipment_reactor_context(ChronicleInProcessFixture fixture) : Specification(fixture)
        {
            public EventSourceId EventSourceId = null!;
            public IEventAppendCollection AppendedEventsCollector = null!;

            public override IEnumerable<Type> EventTypes => [typeof(OrderPlaced), typeof(ShipmentScheduled)];
            public override IEnumerable<Type> Reactors => [typeof(ShipmentReactor)];

            protected override void ConfigureServices(IServiceCollection services) =>
                services.AddSingleton<ShipmentReactor>();

            void Establish() => EventSourceId = EventSourceId.New();

            void Destroy() => AppendedEventsCollector?.Dispose();
        }
    }
}
```

</TabItem>
</Tabs>

The spec, waiting for both the original event and the reactor's follow-up to be captured:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
namespace Cratis.Chronicle.Docs.EventAppendCollection
{
    [Collection(ChronicleCollection.Name)]
    public class and_collecting_the_scheduled_shipment(and_collecting_the_scheduled_shipment.context context)
        : Given<and_collecting_the_scheduled_shipment.context>(context)
    {
#pragma warning disable CS8981 // "context" is a conventional, lowercase BDD nested-class name
        public class context(ChronicleInProcessFixture fixture) : given.a_shipment_reactor_context(fixture)
        {
            async Task Because()
            {
                var reactor = EventStore.Reactors.GetHandlerFor<ShipmentReactor>();
                await reactor.WaitTillActive();

                AppendedEventsCollector = StartCollectingAppends();
                await EventStore.EventLog.Append(EventSourceId, new OrderPlaced("order-123"));

                // Wait for the reactor's follow-up append to arrive
                await AppendedEventsCollector.WaitForCount(2);
            }
        }
#pragma warning restore CS8981

        AppendedEventWithResult Shipment => Context.AppendedEventsCollector.All
            .First(e => e.Event.Content is ShipmentScheduled);

        [Fact] void should_schedule_a_shipment() =>
            Shipment.Event.Content.ShouldBeOfExactType<ShipmentScheduled>();
        [Fact] void should_carry_the_order_id() =>
            ((ShipmentScheduled)Shipment.Event.Content).OrderId.ShouldEqual("order-123");
        [Fact] void should_be_successful() =>
            Shipment.Result.IsSuccess.ShouldBeTrue();
        [Fact] void should_have_a_valid_sequence_number() =>
            Shipment.Result.SequenceNumber.IsActualValue.ShouldBeTrue();
    }
}
```

</TabItem>
</Tabs>

`WaitForCount` accepts an optional `TimeSpan` timeout (default: 5 seconds) and throws
`TimeoutException` if the expected count is not reached in time. `WaitTillActive()` ensures the
reactor is registered and listening on the server before the test appends the triggering event.

## Checking Violations

When a reactor appends directly and the append is rejected by a constraint, the violation is captured
on the `AppendedEventWithResult.Result` instead of throwing. The events, constraint, and reactor:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
namespace Cratis.Chronicle.Docs.EventAppendCollection
{
    [EventType]
    public record UniqueValueRecorded(string UniqueValue);

    [EventType]
    public record UniqueValueFollowUp(string UniqueValue);

    public class UniqueValueFollowUpConstraint : IConstraint
    {
        public void Define(IConstraintBuilder builder) =>
            builder.Unique(b => b.On<UniqueValueFollowUp>(e => e.UniqueValue));
    }

    public class UniqueValueReactor(IEventLog eventLog) : IReactor
    {
        public Task OnUniqueValueRecorded(UniqueValueRecorded evt, EventContext ctx) =>
            eventLog.Append(ctx.EventSourceId, new UniqueValueFollowUp(evt.UniqueValue));
    }
}
```

</TabItem>
</Tabs>

The shared `given` context:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
namespace Cratis.Chronicle.Docs.EventAppendCollection
{
    namespace given
    {
        public class a_unique_value_reactor_context(ChronicleInProcessFixture fixture) : Specification(fixture)
        {
            public IEventAppendCollection AppendedEventsCollector = null!;

            public override IEnumerable<Type> EventTypes => [typeof(UniqueValueRecorded), typeof(UniqueValueFollowUp)];
            public override IEnumerable<Type> ConstraintTypes => [typeof(UniqueValueFollowUpConstraint)];
            public override IEnumerable<Type> Reactors => [typeof(UniqueValueReactor)];

            protected override void ConfigureServices(IServiceCollection services) =>
                services.AddSingleton<UniqueValueReactor>();

            void Destroy() => AppendedEventsCollector?.Dispose();
        }
    }
}
```

</TabItem>
</Tabs>

The spec appends the same unique value from two different event sources — the reactor's second
follow-up violates the uniqueness constraint, so `HasConstraintViolations` is `true` and `IsSuccess`
is `false` for that entry, while the append itself still completes and is captured in the collection:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
namespace Cratis.Chronicle.Docs.EventAppendCollection
{
    [Collection(ChronicleCollection.Name)]
    public class and_a_reactor_directly_appends_a_duplicate_unique_value(and_a_reactor_directly_appends_a_duplicate_unique_value.context context)
        : Given<and_a_reactor_directly_appends_a_duplicate_unique_value.context>(context)
    {
#pragma warning disable CS8981 // "context" is a conventional, lowercase BDD nested-class name
        public class context(ChronicleInProcessFixture fixture) : given.a_unique_value_reactor_context(fixture)
        {
            public string UniqueValue = null!;

            async Task Because()
            {
                var reactor = EventStore.Reactors.GetHandlerFor<UniqueValueReactor>();
                await reactor.WaitTillActive();

                UniqueValue = Guid.NewGuid().ToString();
                var firstEventSourceId = EventSourceId.New();
                var secondEventSourceId = EventSourceId.New();

                AppendedEventsCollector = StartCollectingAppends();
                await EventStore.EventLog.Append(firstEventSourceId, new UniqueValueRecorded(UniqueValue));
                await EventStore.EventLog.Append(secondEventSourceId, new UniqueValueRecorded(UniqueValue));
                await AppendedEventsCollector.WaitForCount(4);
            }
        }
#pragma warning restore CS8981

        AppendedEventWithResult ViolatingAppend => Context.AppendedEventsCollector.All
            .First(e => e.Result.HasConstraintViolations);

        [Fact] void should_have_a_constraint_violation() =>
            ViolatingAppend.Result.HasConstraintViolations.ShouldBeTrue();
        [Fact] void should_not_be_successful() =>
            ViolatingAppend.Result.IsSuccess.ShouldBeFalse();
        [Fact] void should_have_attempted_the_follow_up_event() =>
            ViolatingAppend.Event.Content.ShouldBeOfExactType<UniqueValueFollowUp>();
    }
}
```

</TabItem>
</Tabs>

## Full Example

The `Waiting for Asynchronous Appends` example above is itself a complete, runnable integration test
that verifies a reactor that listens for `OrderPlaced` and appends a `ShipmentScheduled` follow-up
event — the three tabs together (events and reactor, `given` context, and spec) are everything you
need in a real test project.
