TaskHub.Shared

Persistence

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.

🎯 Design Goals

  1. The Repository owns reads. The Unit of Work owns writes. Handlers never call DbContext.SaveChanges() directly β€” they go through IUnitOfWork.SaveAsync(), which integrates the Outbox automatically.
  2. Aggregates are persistence-aware, but ignorant of EF. Domain types implement IAggregate and raise IDomainEvents; the persistence layer harvests them at save time.
  3. Outbox or it didn’t happen. Domain events that cross service boundaries go through the Outbox table, written in the same transaction as the data change. No β€œfire and forget.”
  4. One DbContext per aggregate root family. Cross-context transactions are an anti-pattern.

πŸ“¦ Modules

Core & Repository

| Module | Purpose | | :β€” | :β€” | | Persistence Abstractions | IRepository, IReadRepository<TId, T>, IWriteRepository<TId, T>, IOwnershipRepository<TItem, TOwner>, IUnitOfWork. | | Generic Repository | Generic patterns and registration for repository implementations. |

Database Providers

| Module | Purpose | | :β€” | :β€” | | Entity Framework Core | ContextBase, UnitOfWorkBase, automatic outbox/media wiring, configuration scanning. | | Redis | Connection multiplexer setup, options, integration with IDistributedCache. |

Reliability

| Module | Purpose | | :β€” | :β€” | | Outbox Abstractions | OutboxMessage, IOutboxReader, IOutboxWriter, IOutboxMessageFactory. | | Outbox EF Core | EF Core implementation: schema, reader, processing loop. |

Domain-Specific

| Module | Purpose | | :β€” | :β€” | | Media | Tracks media file metadata (hashes, owner, content type) alongside business data. |

🧭 The Save Pipeline

   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.

βš™οΈ Minimal Configuration

"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_"
}

βœ… Best Practices

πŸ”— See Also