Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Eventuous/eventuous/llms.txt

Use this file to discover all available pages before exploring further.

CommandServiceFixture is an integration-testing helper that runs your command service against a real IEventStore implementation — by default the in-memory store bundled in Eventuous.Testing — and gives you a fluent Given/When/Then API to express the full command-handling cycle. Unlike AggregateSpec, which operates on the aggregate in isolation, the fixture exercises the entire command pipeline: event loading, domain execution, persistence, and result inspection.

NuGet package

<PackageReference Include="Eventuous.Testing" Version="*" />

InMemoryEventStore

Before looking at the fixture itself, it is worth understanding the store it is backed by:
public class InMemoryEventStore : IEventStore
InMemoryEventStore is a fully functional, thread-safe implementation of IEventStore that keeps all streams in a ConcurrentDictionary held in process memory. It supports the complete IEventStore surface — append, read, read backwards, truncate, delete, and optimistic-concurrency checking via ExpectedStreamVersion. Because it is real event-store behaviour without a network round-trip, tests that use it run quickly and require no external infrastructure. You can instantiate it directly and share it between the fixture and any subscriptions or projections under test:
protected IEventStore Store { get; } = new InMemoryEventStore();
WrongVersion is the exception thrown when an ExpectedStreamVersion constraint is violated, mirroring the behaviour you would see with a real store.

CommandServiceFixture entry points

Static factory — bring your own store

public static class CommandServiceFixture
{
    public static IServiceFixtureGiven<TState> ForService<TState>(
        Func<ICommandService<TState>> serviceFactory,
        IEventStore store)
        where TState : State<TState>, new()
}
ForService is the recommended entry point when you want to share a single InMemoryEventStore instance across multiple fixtures or have it participate in subscription tests. Pass a factory delegate that constructs your command service and the IEventStore instance the fixture should use for both seeding prior events and reading back the stream after the command executes.

Direct constructor — auto-created store

public CommandServiceFixture<TState>(
    Func<IEventStore, ITypeMapper, ICommandService<TState>> serviceFactory)
Instantiate CommandServiceFixture<TState> directly when you want the fixture to own its own InMemoryEventStore. The store and a default TypeMapper are constructed internally and passed to your factory delegate, so you do not need to manage them yourself. Use this form when the test does not need to inspect or share the underlying store.

Fluent API — Given / When / Then

The fixture chains three builder interfaces. Each must be called in order.

IServiceFixtureGiven<TState> — seed the stream

// Seed by explicit stream name
IServiceFixtureWhen<TState> Given(StreamName streamName, params object[] events);

// Seed by entity id (stream name derived by convention from TState)
IServiceFixtureWhen<TState> Given(string id, params object[] events);
Call Given with either a StreamName or a plain string entity id. The events you provide are appended to that stream before the command is dispatched. Pass no events (or an empty array) to represent a brand-new entity that has no prior history.

IServiceFixtureWhen<TState> — issue the command

IServiceFixtureThen<TState> When<TCommand>(TCommand command) where TCommand : class;
Passes command to ICommandService<TState>.Handle. The service is called asynchronously; the returned task is captured internally and awaited when Then or ThenAsync is called.

IServiceFixtureThen<TState> — assert the outcome

// Synchronous assertions
Task Then(Action<CommandServiceFixture<TState>.FixtureResult> assert);

// Asynchronous assertions
Task ThenAsync(Func<CommandServiceFixture<TState>.FixtureResult, Task> assert);
Both overloads await the command result, read the entire stream from the store, and hand a FixtureResult to your assertion delegate.

FixtureResult — assertion helpers

FixtureResult bundles the service Result<TState> and the full stream contents. It exposes chainable assertion methods:

Result assertions

MethodBehaviour
ResultIsOk(Action<Result<TState>.Ok>? assert = null)Fails if the result is not Ok; optionally runs additional assertions on the success value.
ResultIsOkAsync(Func<Result<TState>.Ok, Task>? assert = null)Async variant of ResultIsOk.
ResultIsError(Action<Result<TState>.Error>? assert = null)Fails if the result is not an error; optionally inspects the error.
ResultIsError<T>(Action<T>? assert = null)Fails unless the result is an error and its exception is of type T.

Stream assertions

MethodBehaviour
FullStreamEventsAre(params object[] events)Asserts that the entire stream (seeded events plus newly appended events) equals events in order.
NewStreamEventsAre(params object[] events)Asserts that only the newly appended events (those written by the command handler) equal events in order.
StreamIs(Action<StreamEvent[]> assert)Passes the raw StreamEvent[] array to your assertion delegate for full flexibility.
StreamIsAsync(Func<StreamEvent[], Task> assert)Async variant of StreamIs.
All assertion methods return FixtureResult (or Task<FixtureResult>) for chaining.

AggregateFactoryExtensions

When your tests need to construct an aggregate with a pre-set identity outside of the spec pattern, AggregateFactoryExtensions adds the following extension method to AggregateFactoryRegistry:
public TAggregate CreateTestAggregateInstance<TAggregate, TState, TId>(TId id)
    where TAggregate : Aggregate<TState>
    where TState     : State<TState>, new()
    where TId        : Id
This creates an aggregate via the registry and then assigns the provided id to the instance, which is useful when writing fixture setup helpers that need a fully initialised aggregate without loading events.

Complete example

The following example tests a BookingsCommandService end-to-end.

Domain and application setup

using Bookings.Domain.Bookings;
using Eventuous;
using NodaTime;
using static Bookings.Application.BookingCommands;

// ──── Events ────────────────────────────────────────────────────────────────

[EventType("V1.RoomBooked")]
public record RoomBooked(
    string    RoomId,
    LocalDate CheckIn,
    LocalDate CheckOut,
    float     Price);

[EventType("V1.PaymentRecorded")]
public record PaymentRecorded(float PaidAmount, float Outstanding, string PaymentId);

[EventType("V1.BookingFullyPaid")]
public record BookingFullyPaid(DateTimeOffset FullyPaidAt);

// ──── State ─────────────────────────────────────────────────────────────────

public record BookingState : State<BookingState>
{
    public float Outstanding { get; init; }
    public bool  Paid        { get; init; }

    public BookingState()
    {
        On<RoomBooked>((s, e) => s with { Outstanding = e.Price });
        On<PaymentRecorded>((s, e) => s with { Outstanding = e.Outstanding });
        On<BookingFullyPaid>((s, _) => s with { Paid = true });
    }
}

// ──── Aggregate ─────────────────────────────────────────────────────────────

public class Booking : Aggregate<BookingState>
{
    public void BookRoom(string roomId, LocalDate checkIn, LocalDate checkOut, float price)
    {
        EnsureDoesntExist();
        Apply(new RoomBooked(roomId, checkIn, checkOut, price));
    }

    public void RecordPayment(float amount, string paymentId, DateTimeOffset paidAt)
    {
        EnsureExists();
        var outstanding = State.Outstanding - amount;
        Apply(new PaymentRecorded(amount, outstanding, paymentId));
        if (outstanding <= 0) Apply(new BookingFullyPaid(paidAt));
    }
}

public record BookingId(string Value) : Id(Value);

// ──── Commands ───────────────────────────────────────────────────────────────

public static class BookingCommands
{
    public record BookRoom(
        string    BookingId,
        string    RoomId,
        LocalDate CheckIn,
        LocalDate CheckOut,
        float     Price);

    public record RecordPayment(
        string         BookingId,
        float          Amount,
        string         PaymentId,
        DateTimeOffset PaidAt);
}

// ──── Command service ────────────────────────────────────────────────────────

public class BookingsCommandService : CommandService<Booking, BookingState, BookingId>
{
    public BookingsCommandService(IEventStore store) : base(store)
    {
        On<BookRoom>()
            .InState(ExpectedState.New)
            .GetId(cmd => new BookingId(cmd.BookingId))
            .Act((booking, cmd) =>
                booking.BookRoom(cmd.RoomId, cmd.CheckIn, cmd.CheckOut, cmd.Price));

        On<RecordPayment>()
            .InState(ExpectedState.Existing)
            .GetId(cmd => new BookingId(cmd.BookingId))
            .Act((booking, cmd) =>
                booking.RecordPayment(cmd.Amount, cmd.PaymentId, cmd.PaidAt));
    }
}

Test class

using Eventuous.Testing;

public class BookingCommandServiceTests
{
    // Each test class instance gets its own fresh store (most runners create one instance per test)
    readonly InMemoryEventStore _store = new();

    ICommandService<BookingState> CreateService()
        => new BookingsCommandService(_store);

    // ── Test 1: booking a new room emits RoomBooked ──────────────────────────

    [Test]
    public async Task Should_book_room_on_new_stream()
    {
        var checkIn  = LocalDate.FromDateTime(DateTime.Today);
        var checkOut = checkIn.PlusDays(3);
        var cmd      = new BookRoom("booking-1", "room-42", checkIn, checkOut, 300f);
        var expected = new RoomBooked("room-42", checkIn, checkOut, 300f);

        await CommandServiceFixture
            .ForService(CreateService, _store)
            .Given(cmd.BookingId)               // no prior events — new stream
            .When(cmd)
            .ThenAsync(async result =>
            {
                await result.ResultIsOkAsync(async ok =>
                    await Assert.That(ok.Changes).HasCount(1));

                result.FullStreamEventsAre(expected);
            });
    }

    // ── Test 2: recording a payment against an existing booking ──────────────

    [Test]
    public async Task Should_record_payment_on_existing_booking()
    {
        var checkIn  = LocalDate.FromDateTime(DateTime.Today);
        var checkOut = checkIn.PlusDays(3);
        var paidAt   = DateTimeOffset.UtcNow;

        var seed     = new RoomBooked("room-42", checkIn, checkOut, 300f);
        var cmd      = new RecordPayment("booking-2", 300f, "pay-001", paidAt);

        await CommandServiceFixture
            .ForService(CreateService, _store)
            .Given("booking-2", seed)           // pre-seed with an existing booking
            .When(cmd)
            .Then(result =>
                result
                    .ResultIsOk()
                    .NewStreamEventsAre(
                        new PaymentRecorded(300f, 0f, "pay-001"),
                        new BookingFullyPaid(paidAt)));
    }

    // ── Test 3: booking when the stream already exists should fail ────────────

    [Test]
    public async Task Should_fail_when_booking_already_exists()
    {
        var checkIn  = LocalDate.FromDateTime(DateTime.Today);
        var checkOut = checkIn.PlusDays(3);
        var seed     = new RoomBooked("room-42", checkIn, checkOut, 300f);
        var cmd      = new BookRoom("booking-3", "room-42", checkIn, checkOut, 300f);

        await CommandServiceFixture
            .ForService(CreateService, _store)
            .Given("booking-3", seed)           // stream already exists
            .When(cmd)
            .Then(result => result.ResultIsError<WrongVersion>());
    }
}

Using InMemoryEventStore for subscription tests

Because InMemoryEventStore stores all appended events in a global in-process list, you can also use it to drive subscription and projection tests. Append seed events before starting the subscription, let the handler process them synchronously, then read back the projected state:
var store = new InMemoryEventStore();

// Seed events that a projection should react to
await store.Store(
    StreamName.For<BookingState>("booking-99"),
    ExpectedStreamVersion.NoStream,
    new object[] { new RoomBooked("room-1", checkIn, checkOut, 200f) });

// ... start your subscription handler pointed at `store`, then assert projection state
This approach gives you deterministic, fast subscription integration tests without a running event store server.

Choosing between AggregateSpec and CommandServiceFixture

ConcernAggregateSpecCommandServiceFixture
ScopeAggregate domain logic onlyFull command pipeline
Event storeNone — fully in-memory aggregateInMemoryEventStore (or any IEventStore)
SpeedFastestFast — no network, but includes persistence
What you assertChanges list on the aggregateStream contents and Result<TState>
When to usePure domain logic, invariant checksEnd-to-end handler correctness, result shape, metadata

Build docs developers (and LLMs) love