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
| Method | Behaviour |
|---|
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
| Method | Behaviour |
|---|
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
| Concern | AggregateSpec | CommandServiceFixture |
|---|
| Scope | Aggregate domain logic only | Full command pipeline |
| Event store | None — fully in-memory aggregate | InMemoryEventStore (or any IEventStore) |
| Speed | Fastest | Fast — no network, but includes persistence |
| What you assert | Changes list on the aggregate | Stream contents and Result<TState> |
| When to use | Pure domain logic, invariant checks | End-to-end handler correctness, result shape, metadata |