Sample 3 — Outbox + Worker
Concept: Outbox + message bus + two background workers (async dispatch). What changes when commands stop running in the caller's thread.
- Code:
samples/Stratara.Sample.OutboxWorker - Lines: ~300
- Read time: 15–20 min
- Prerequisite: Sample 2 — Event Sourced.
What you'll see
InMemoryOutbox— the sample's stand-in for theoutbox_entrytable. Same semantics as the EF Core impl.InMemoryMessageBus— pub/sub on top of aChannel<>. Stands in for RabbitMQ / Azure Service Bus.- Two
IHostedServices running concurrently:- OutboxWorker — polls the outbox, publishes pending commands to the bus.
- CommandWorker — subscribes to the bus, deserializes commands, dispatches them via
IMediatorto handlers.
- Asynchronous semantics — the caller doesn't
awaitthe handler. The producer is the thing that enqueues to the outbox; the consumer is the thing that picks it up later.
Running
dotnet run --project samples/Stratara.Sample.OutboxWorker
Expected output (abridged):
=== Stratara Outbox + Worker ===
--- Publisher enqueues 3 commands (returns immediately, doesn't wait for handlers) ---
Enqueued — outbox has 3 pending
--- Wait for outbox-drain + command-worker to catch up ---
Outbox now has 0 pending
--- Read-side: query the repository synchronously ---
Balance: $175.00
--- Enqueue WithdrawCommand $40, wait, query again ---
Balance: $135.00
Done.
What changed vs. Sample 2
| Sample 2 (sync event-sourced) | Sample 3 (async via outbox) |
|---|---|
mediator.HandleAsync(cmd) runs the handler in the caller's thread |
dispatcher.EnqueueAsync(cmd) returns immediately after appending to the outbox |
| Handler exceptions bubble back to the caller | Handler exceptions are caught + the outbox-entry retried (with backoff) |
| Strict ordering — caller controls when the next command runs | At-least-once delivery — consumers must be idempotent |