Concepts
CQRS and domain events
How commands, queries and events flow through Wolverine, and the transactional outbox that makes messaging reliable.
On this page
Commands and queries
Slicekit uses Wolverine as its mediator and message bus. A command changes state; a query reads it. Both are messages, dispatched to a handler discovered by convention.
// command
await bus.InvokeAsync(new SendInvoice(invoiceId));
// query
var invoice = await bus.InvokeAsync<InvoiceDto>(new GetInvoice(invoiceId));
Handlers are static methods that take the message plus whatever services they need as parameters. Dependencies are injected per call, so handlers stay easy to test in isolation.
Raise vs publish
The two verbs are not interchangeable, and keeping them distinct keeps the model honest:
- Aggregates raise events. Raising records a fact inside the domain; it does not send anything.
- Dispatchers publish events. After the change is committed, raised events are published to handlers and to the outbox.
// inside the aggregate
project.Raise(new ProjectArchived(project.Id));
// the infrastructure publishes it after SaveChanges
await publisher.PublishAsync(raisedEvent);
The transactional outbox
Publishing an event and committing the database change must not drift apart. Wolverine’s outbox stores outgoing messages in the same transaction as the state change, then relays them to RabbitMQ once the transaction commits. If the process dies mid-flight, the relay picks up where it left off: no lost messages, no double sends.
Aggregate raises
records a fact in the domain
One transaction
state + outbox row commit together
Relay forwards
resumes after a crash
RabbitMQ
integration events
Handlers react
at least once
Integration events
Some events cross service boundaries. Those are published to RabbitMQ as integration events and consumed by handlers, in this app or another. The outbox guarantees at-least-once delivery: an event is never lost, and consumers are written idempotent so a rare redelivery is harmless.
Handling an event
A handler subscribes simply by accepting the event type:
public static class WhenProjectArchived
{
public static async Task Handle(ProjectArchived e, AppDbContext db, CancellationToken ct)
{
// react: revoke access, send a notification, update a read model…
}
}
Where audit fits
Audit events ride the same logging pipeline rather than a bespoke table; they are emitted through Serilog and shipped to Loki. Retention is an operations concern, not an application one. See observability.