This challenge demonstrates how to implement Domain Events and the Outbox Pattern using Entity Framework Core with SQLite and Background Services in .NET 9.
It ensures domain events are saved atomically with aggregate changes and then published reliably from an Outbox table by a background dispatcher.
Component | Purpose |
---|---|
.NET 9 | Framework |
EF Core + SQLite | Persistence layer |
SaveChangesInterceptor | Captures domain events before save |
BackgroundService | Dispatches outbox messages |
System.Text.Json | Serializes domain events |
Swashbuckle.AspNetCore | Swagger UI for testing endpoints |
┌────────────────────────────────────────┐
│ API + EF Core │
│ ───────────────────────────────────── │
│ Order Aggregate raises Domain Event │
│ → EF Interceptor saves Outbox row │
│ → Transaction commits atomically │
└──────────────┬─────────────────────────┘
│
▼
📨 Outbox Dispatcher (Hosted Service)
├─ Reads unprocessed messages
├─ Publishes events (console bus)
└─ Marks as processed
Challenge05_Outbox/
│
├── Program.cs
│
├── Domain/
│ ├── DomainEvent.cs
│ ├── IHasDomainEvents.cs
│ ├── Order.cs
│ └── OrderPlacedDomainEvent.cs
│
└── Infrastructure/
├── AppDbContext.cs
├── OutboxMessage.cs
├── OutboxDispatcher.cs
├── OutboxSaveChangesInterceptor.cs
├── IEventSerializer.cs
└── SystemTextJsonEventSerializer.cs
git clone https://github.com/YOUR_USERNAME/Challenge05_Outbox.git
cd Challenge05_Outbox
dotnet restore
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Swashbuckle.AspNetCore
dotnet tool install --global dotnet-ef
dotnet ef migrations add Init
dotnet ef database update
This will create a local SQLite file: challenge05.db
dotnet run
Open:
- Swagger UI → http://localhost:5000/swagger
curl -X POST "http://localhost:5000/orders?total=49.99"
curl "http://localhost:5000/outbox"
Console output example:
[BUS] ✅ OrderPlaced published => OrderId=6fdc..., Total=49.99
Table | Purpose |
---|---|
Orders | Domain aggregate |
OutboxMessages | Stores serialized domain events |
SQLite DDL:
CREATE TABLE "OutboxMessages" (
"Id" INTEGER PRIMARY KEY AUTOINCREMENT,
"OccurredOnUtc" TEXT NOT NULL,
"Type" TEXT NOT NULL,
"Payload" TEXT NOT NULL,
"ProcessedOnUtc" TEXT NULL,
"Error" TEXT NULL
);
CREATE INDEX "IX_OutboxMessages_ProcessedOnUtc"
ON "OutboxMessages" ("ProcessedOnUtc");
-
Aggregate raises event
AddEvent(new OrderPlacedDomainEvent(Id, Total));
-
Interceptor captures it during
SaveChangesAsync
and writes anOutboxMessage
in the same transaction. -
Background service polls for unprocessed messages and “publishes” them (here, simulated with console output).
-
Marks processed →
ProcessedOnUtc
is updated.
- OutboxSaveChangesInterceptor.cs → Captures domain events pre-save
- OutboxDispatcher.cs → Processes messages asynchronously
- SystemTextJsonEventSerializer.cs → Serializes/deserializes events
- AppDbContext.cs → Entity mappings + EF setup
Package | Purpose |
---|---|
Microsoft.EntityFrameworkCore.Sqlite | SQLite provider |
Microsoft.EntityFrameworkCore.Design | CLI migrations |
Swashbuckle.AspNetCore | Swagger documentation |
Microsoft.Extensions.Hosting | Background services |
Microsoft.Extensions.Logging.Console | Logging |
- Add retry & backoff policy in dispatcher
- Use a real message bus (e.g., RabbitMQ or Kafka)
- Introduce event versioning and handlers registry
- Add unit tests with InMemory EF provider
This project is licensed under the MIT License. Feel free to use / modify / share.
Spyros Ponaris 💼 LinkedIn