Skip to content

Event Sequence Assertions

After appending events through an EventScenario, you often need to verify what ended up in the event sequence itself — not just the result of the append call. The Cratis.Chronicle.Testing package provides Should* extension methods on IEventSequence and IEventLog for these assertions, as well as Should* extension methods on AppendedEventWithResult for asserting on collected events.

These methods are available on any IEventSequence or IEventLog instance, including scenario.EventLog and scenario.EventSequence.

MethodAsserts
ShouldHaveTailSequenceNumber(expected)Tail sequence number matches the expected value
ShouldHaveAppendedEvent<TEvent>()At least one event of the given type exists anywhere in the sequence
ShouldHaveAppendedEvent<TEvent>(validator)At least one event of the given type exists and passes the validator
ShouldHaveAppendedEvent<TEvent>(predicate)At least one event of the given type matches the predicate
ShouldHaveAppendedEvent<TEvent>(eventSourceId)At least one event of the given type exists for the event source
ShouldHaveAppendedEvent<TEvent>(eventSourceId, validator)At least one event of the given type exists for the event source and passes the validator
ShouldHaveAppendedEvent<TEvent>(eventSourceId, predicate)At least one event of the given type matches the predicate for the event source
ShouldHaveAppendedEvent<TEvent>(sequenceNumber)An event of the given type exists at the sequence number
ShouldHaveAppendedEvent<TEvent>(sequenceNumber, validator)An event of the given type exists at the sequence number and passes the validator
ShouldHaveAppendedEvent<TEvent>(sequenceNumber, predicate)An event of the given type at the sequence number matches the predicate
ShouldHaveAppendedEvent<TEvent>(sequenceNumber, eventSourceId, validator)An event of the given type exists at the sequence number for the given event source and passes the validator
ShouldHaveAppendedEvent<TEvent>(sequenceNumber, eventSourceId, predicate)An event of the given type at the sequence number for the given event source matches the predicate

All methods are async and return Task. They throw EventSequenceAssertionException on failure with a descriptive message.

The tail sequence number is the sequence number of the last event appended to the sequence. Use ShouldHaveTailSequenceNumber to verify the expected number of events were appended:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Testing.EventSequences;
[EventType]
public record TestingSeqAssertAuthorRegistered(string Name);
[EventType]
public record TestingSeqAssertBookAdded(string Title);
public static class TestingSeqAssertTailSequenceNumber
{
public static async Task Run()
{
var scenario = new EventScenario();
var authorId = EventSourceId.New();
await scenario.EventLog.Append(authorId, new TestingSeqAssertAuthorRegistered("Jane Smith"));
await scenario.EventLog.Append(authorId, new TestingSeqAssertBookAdded("Clean Code"));
await scenario.EventLog.ShouldHaveTailSequenceNumber(1);
}
}

Sequence numbers are zero-based, so the first event has sequence number 0 and the second has 1. The special value EventSequenceNumber.Unavailable indicates no events have been appended.

Use ShouldHaveAppendedEvent<TEvent> without a sequence number to verify that at least one event of the expected type was appended anywhere in the sequence:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Testing.EventSequences;
public static class TestingSeqAssertAppendedEventByType
{
public static async Task Run()
{
var scenario = new EventScenario();
var authorId = EventSourceId.New();
await scenario.EventLog.Append(authorId, new TestingSeqAssertAuthorRegistered("Jane Smith"));
await scenario.EventLog.Append(authorId, new TestingSeqAssertBookAdded("Clean Code"));
await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>();
await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertBookAdded>();
}
}

When you know the exact position, pass a sequence number to assert at a specific location:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Testing.EventSequences;
public static class TestingSeqAssertAppendedEventAtPosition
{
public static async Task Run(EventScenario scenario)
{
await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(0);
await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertBookAdded>(1);
}
}

Pass a validator action to inspect the event content. The assertion fails if the event is not of the expected type or if the validator throws:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Testing.EventSequences;
using Cratis.Specifications;
public static class TestingSeqAssertValidator
{
public static async Task Run()
{
var scenario = new EventScenario();
var authorId = EventSourceId.New();
await scenario.EventLog.Append(authorId, new TestingSeqAssertAuthorRegistered("Jane Smith"));
await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(0, author =>
author.Name.ShouldEqual("Jane Smith"));
}
}

Without a sequence number, the validator runs against the first event of the matching type:

using Cratis.Chronicle.Testing.EventSequences;
using Cratis.Specifications;
public static class TestingSeqAssertValidatorNoSequence
{
public static Task Run(EventScenario scenario) =>
scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(author =>
author.Name.ShouldEqual("Jane Smith"));
}

Pass a Func<TEvent, bool> predicate when you only need to check whether the event satisfies a condition. The assertion fails if no event of the expected type returns true from the predicate:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Testing.EventSequences;
public static class TestingSeqAssertPredicate
{
public static async Task Run()
{
var scenario = new EventScenario();
var authorId = EventSourceId.New();
await scenario.EventLog.Append(authorId, new TestingSeqAssertAuthorRegistered("Jane Smith"));
// Without sequence number — finds any matching event
await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(
author => author.Name == "Jane Smith");
// At a specific sequence number
await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(0,
author => author.Name == "Jane Smith");
}
}

Verifying event content for a specific event source

Section titled “Verifying event content for a specific event source”

When events for multiple event sources exist in the same sequence, filter by event source to avoid ambiguity. All event source overloads support both Action<TEvent> validators and Func<TEvent, bool> predicates:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Testing.EventSequences;
using Cratis.Specifications;
public static class TestingSeqAssertByEventSource
{
public static async Task Run()
{
var scenario = new EventScenario();
var author1 = EventSourceId.New();
var author2 = EventSourceId.New();
await scenario.EventLog.Append(author1, new TestingSeqAssertAuthorRegistered("Jane Smith"));
await scenario.EventLog.Append(author2, new TestingSeqAssertAuthorRegistered("John Doe"));
// With sequence number and validator
await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(0, author1, author =>
author.Name.ShouldEqual("Jane Smith"));
// Without sequence number — finds any matching event for the event source
await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(author2, author =>
author.Name.ShouldEqual("John Doe"));
// With a predicate instead of a validator
await scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(
author1, author => author.Name == "Jane Smith");
}
}

When using IEventAppendCollection to collect events (see Event Append Collection), each captured entry is an AppendedEventWithResult. The testing package provides Should* extensions for asserting directly on these entries.

All append result assertions are available directly on AppendedEventWithResult — they delegate to the inner Result:

MethodAsserts
ShouldBeSuccessful()Operation succeeded with no violations or errors
ShouldBeFailed()Operation failed (any violation or error)
ShouldHaveConstraintViolations()At least one constraint violation is present
ShouldNotHaveConstraintViolations()No constraint violations are present
ShouldHaveConstraintViolationFor(name)A violation for the named constraint is present
ShouldHaveConcurrencyViolations()At least one concurrency violation is present
ShouldNotHaveConcurrencyViolations()No concurrency violations are present
ShouldHaveErrors()At least one error is present
ShouldNotHaveErrors()No errors are present
MethodAsserts
ShouldHaveEvent<TEvent>(validate?)Event content is of the given type; optional validator inspects the content
ShouldBeForEventSource(eventSourceId)Event was appended for the given event source
using System.Linq;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.Testing.EventSequences;
using Cratis.Specifications;
public static class TestingSeqAssertAppendedEventWithResult
{
public static async Task Run()
{
var scenario = new EventScenario();
var authorId = EventSourceId.New();
var result = await scenario.EventLog.Append(authorId, new TestingSeqAssertAuthorRegistered("Jane Smith"));
var appendedEvents = await scenario.EventLog.GetFromSequenceNumber(EventSequenceNumber.First, authorId);
var collected = new AppendedEventWithResult(appendedEvents.Last(), result);
collected.ShouldBeSuccessful();
collected.ShouldHaveEvent<TestingSeqAssertAuthorRegistered>(author =>
author.Name.ShouldEqual("Jane Smith"));
collected.ShouldBeForEventSource(authorId);
}
}

The following test pre-seeds state, appends two events, and then verifies both the tail sequence number and individual event content:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Testing.EventSequences;
using Cratis.Specifications;
using Xunit;
[EventType]
public record TestingSeqAssertLibraryCreated(string Name);
public record TestingSeqAssertAuthorId(Guid Value) : EventSourceId<Guid>(Value)
{
public static TestingSeqAssertAuthorId New() => new(Guid.NewGuid());
}
public class when_registering_an_author_and_adding_a_book
{
readonly EventScenario _scenario = new();
[Fact]
public async Task should_append_both_events_in_order()
{
var authorId = TestingSeqAssertAuthorId.New();
await _scenario.Given
.ForEventSource(authorId)
.Events(new TestingSeqAssertLibraryCreated("Main Library"));
await _scenario.EventLog.Append(authorId, new TestingSeqAssertAuthorRegistered("Jane Smith"));
await _scenario.EventLog.Append(authorId, new TestingSeqAssertBookAdded("Clean Code"));
// Tail includes the seeded event (0) plus the two appended events (1, 2)
await _scenario.EventLog.ShouldHaveTailSequenceNumber(2);
await _scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertAuthorRegistered>(1, author =>
author.Name.ShouldEqual("Jane Smith"));
await _scenario.EventLog.ShouldHaveAppendedEvent<TestingSeqAssertBookAdded>(2, book =>
book.Title.ShouldEqual("Clean Code"));
}
}

For asserting on the result of an individual append operation, see Append Assertions.