Command Handling with MediatR¶
EventSourcingKit deals in events, not commands: it stores what has happened and replays it. The step before that, turning an intent into events, is the job of a command handler. MediatR is a common way to structure those handlers, and it fits EventSourcingKit naturally.
The Pattern¶
A command names something a user wants to do, such as registering a book. With MediatR, a command is an IRequest, and its handler is an IRequestHandler that injects IEventStore. The handler validates the request, then stores one or more events:
public record RegisterBookCommand(string Title, string Author) : IRequest<Guid>;
public class RegisterBookCommandHandler(IEventStore eventStore)
: IRequestHandler<RegisterBookCommand, Guid>
{
public async Task<Guid> Handle(
RegisterBookCommand command,
CancellationToken cancellationToken
)
{
var bookId = Guid.CreateVersion7();
var subject = new Subject($"/books/{bookId}");
var candidate = new EventCandidate(
subject,
new BookRegistered(bookId, command.Title, command.Author)
);
await eventStore.StoreEvents(
[candidate],
[new IsSubjectPristinePrecondition(subject)],
cancellationToken
);
return bookId;
}
}
Deciding against Current State¶
When a command depends on what has already happened, load the state first. Replay the subject, check the invariant, then store the new event guarded by a precondition. This is the load, decide, store cycle:
var book = await eventStore.ReplayToCurrentState<BookState, Guid>(
subject,
cancellationToken
);
if (book.IsCheckedOut)
{
throw new InvalidOperationException("The book is already checked out.");
}
await eventStore.StoreEvents(
[new EventCandidate(subject, new BookCheckedOut(book.Id, userId))],
[new IsSubjectPopulatedPrecondition(subject)],
cancellationToken
);
Registration¶
Register MediatR alongside EventSourcingKit, pointing it at the assembly that contains your handlers:
Both libraries take the same assembly, so your events, reducers, and command handlers become available together.
For More Information¶
- Storing Events and Preconditions explains the precondition guards used above.
- State and Reducers explains the replay step.
- Exposing an API sends commands from a controller or resolver.