The Persistence layer is TaskHubβs standardized contract with data stores. It enforces a small set of patterns β Repository + Unit of Work, Outbox for cross-service events, EF Core for relational data, Redis for caching β and provides reusable base classes that handle the wiring.
DbContext.SaveChanges() directly β they go through IUnitOfWork.SaveAsync(), which integrates the Outbox automatically.IAggregate and raise IDomainEvents; the persistence layer harvests them at save time.| Module | Purpose |
| :β | :β |
| Persistence Abstractions | IRepository, IReadRepository<TId, T>, IWriteRepository<TId, T>, IOwnershipRepository<TItem, TOwner>, IUnitOfWork. |
| Generic Repository | Generic patterns and registration for repository implementations. |
| Module | Purpose |
| :β | :β |
| Entity Framework Core | ContextBase, UnitOfWorkBase, automatic outbox/media wiring, configuration scanning. |
| Redis | Connection multiplexer setup, options, integration with IDistributedCache. |
| Module | Purpose |
| :β | :β |
| Outbox Abstractions | OutboxMessage, IOutboxReader, IOutboxWriter, IOutboxMessageFactory. |
| Outbox EF Core | EF Core implementation: schema, reader, processing loop. |
| Module | Purpose | | :β | :β | | Media | Tracks media file metadata (hashes, owner, content type) alongside business data. |
Handler UnitOfWork.SaveAsync()
ββββββββ ββββββββββββββββββββββ
β
aggregate.Foo() βββΊ raises βββΊ β 1. Scan ChangeTracker for IAggregate
aggregate.Bar() βββΊ raises βββΊ β 2. Extract IDomainEvent[] from each
β 3. Convert to OutboxMessage[]
β 4. SaveChangesAsync()
β β business rows + outbox rows
β β single SQL transaction
β 5. ClearEvents() on each aggregate
βΌ
Committed
A separate background worker reads the outbox and publishes to the message bus (RabbitMQ / Kafka / etc.). Even if the worker dies mid-publish, the message stays in the outbox until processed β at-least-once delivery, with the transactional guarantee that no event is ever lost.
"Persistence": {
"ConnectionString": "Host=db;Database=taskhub;Username=app;Password=β¦",
"Outbox": {
"IsEnabled": true,
"TableName": "outbox_messages",
"BatchSize": 100,
"MaxRetryCount": 5,
"ProcessingInterval": "00:00:10"
},
"Media": {
"IsEnabled": true,
"TableName": "media_metadata"
}
},
"Redis": {
"Endpoint": "redis:6379",
"InstanceName": "taskhub_"
}
IUnitOfWork.SaveAsync(). Bypassing it skips the Outbox.DbContext injection, no LINQ β thatβs the repositoryβs job.FullHostBuilder does run migrations on startup, which is convenient for small fleets β but for >3 replicas, run migrations as a separate Job/init container to avoid the startup race.UnitOfWorkBase event harvesting.AggregateBase and the event collection contract this layer reads.