Event Append Collection
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.
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 is:
IEventAppendCollection _appendedEventsCollector;
async Task Because()
{
_appendedEventsCollector = StartCollectingAppends();
await EventStore.EventLog.Append(EventSourceId, new ItemRegistered("Widget"));
}
void Destroy() => _appendedEventsCollector?.Dispose();
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
[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();
Multiple events
When a reactor handles an event and appends a follow-up, all appends land in the same collection. Use LINQ to locate the event you want:
AppendedEventWithResult FollowUp => Context._appendedEventsCollector.All.First(e => e.Event.Content is FollowUpAppended);
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:
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);
}
WaitForCount accepts an optional TimeSpan timeout (default: 5 seconds) and throws
TimeoutException if the expected count is not reached in time.
Checking Violations
When a reactor appends directly and the append is rejected by a constraint, the violation is captured
on the AppendedEventWithResult.Result:
[Fact] void should_have_a_constraint_violation() =>
Context._appendedEventsCollector.All[0].Result.HasConstraintViolations.ShouldBeTrue();
To find the first violation among multiple appends:
AppendedEventWithResult ViolatingAppend => Context._appendedEventsCollector.All.First(e => e.Result.HasConstraintViolations);
Full Example
The following is a complete integration test that verifies a reactor that listens for OrderPlaced
and appends a ShipmentScheduled follow-up event.
Events and reactor:
[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));
}
Given context — shared setup for the test group:
namespace MyApp.Integration.for_ShipmentReactor.given;
public class a_shipment_reactor_context(ChronicleInProcessFixture fixture) : Specification(fixture)
{
public EventSourceId EventSourceId;
public IEventAppendCollection _appendedEventsCollector;
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();
}
Spec:
namespace MyApp.Integration.for_ShipmentReactor.when_an_order_is_placed;
[Collection(ChronicleCollection.Name)]
public class and_collecting_the_scheduled_shipment(context context) : Given<context>(context)
{
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
await _appendedEventsCollector.WaitForCount(2);
}
}
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();
}
WaitTillActive() ensures the reactor is registered and listening on the server before the test
appends the triggering event. WaitForCount(2) then waits for both the original event and the
reactor's follow-up to be captured before assertions run.