Skip to content

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:

builder.Services.AddMediatR(c => c.RegisterServicesFromAssembly(assembly));

Both libraries take the same assembly, so your events, reducers, and command handlers become available together.

For More Information