Skip to content

Feature/ddd#159

Merged
jasonmwebb-lv merged 50 commits intomainfrom
feature/ddd
Mar 24, 2026
Merged

Feature/ddd#159
jasonmwebb-lv merged 50 commits intomainfrom
feature/ddd

Conversation

@jasonmwebb-lv
Copy link
Copy Markdown
Collaborator

Implemented full featured DDD implementation. This is fairly experimental in nature and will need some E2E testing.

jasonmwebb-lv and others added 30 commits March 16, 2026 20:12
Defines the design for AggregateRoot, DomainEntity, ValueObject, and
IDomainEvent types to be added to RCommon.Entities. Extends existing
BusinessEntity hierarchy with zero breaking changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address spec review findings: add [Serializable] and [ConcurrencyCheck],
use block-scoped namespaces, add non-generic IAggregateRoot marker,
fix DomainEvents allocation via dual-list approach, thread-safe
GetHashCode, document graph walker behavior and known limitations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add IEquatable<DomainEntity<TKey>> implementation, default! initializer
for Id, expand known-limitation documentation for dual-list sync bypass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TDD implementation plan for AggregateRoot, DomainEntity, ValueObject,
and IDomainEvent. 5 chunks, 25 steps, 69 tests covering all spec
scenarios including integration with IEntityEventTracker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
IDomainEvent extends ISerializableEvent with EventId and OccurredOn.
DomainEvent is an abstract record with sensible defaults.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
C# record-based value object with automatic structural equality,
immutability, and with-expression support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lightweight entity with identity-based equality and IEquatable support.
No event tracking — child entities raise events through aggregate root.
Extends BusinessEntity<TKey> with domain event management, versioning,
and optimistic concurrency. Domain events flow through existing
IEntityEventTracker pipeline via AddLocalEvent delegation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The 'new' keyword hiding of EntityEquals created polymorphic
inconsistency — callers through IBusinessEntity would get binary
comparison while callers through AggregateRoot got Id comparison.
Better tracked as a future improvement making EntityEquals virtual
on BusinessEntity.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…n, InMemoryEventBus memory leak

- Add ConfigureAwait(false) to ~150 await calls across all library projects
- Add CancellationToken to IEventBus.PublishAsync, IEventRouter.RouteEventsAsync,
  and IEmailService.SendEmailAsync interfaces
- Propagate CancellationToken through DapperRepository.OpenAsync (13 sites),
  Linq2DbRepository.DeleteAsync, all MediatR/Wolverine/MassTransit handler bridges,
  and the event routing pipeline
- Refactor InMemoryEventBus to not capture IServiceCollection (memory leak),
  track dynamic subscriptions internally and resolve via ActivatorUtilities
- Fix SendGridEmailService.SendEmailAsync missing return on empty recipients guard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds three new design sections to the aggregate repository spec:
- Part 2: Automatic domain event dispatch via UnitOfWork post-commit hook
- Part 3: Read-model repositories (IReadModelRepository<T>, IPagedResult<T>)
- Part 4: Saga/process manager patterns (ISaga, IStateMachine, ISagaStore)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… safety

Implement JsonOutboxSerializer as IOutboxSerializer with:
- Serialize: Convert ISerializableEvent to JSON using System.Text.Json
- GetEventTypeName: Return short assembly-qualified type name for deserialization
- Deserialize: Reconstruct ISerializableEvent from type name and payload with validation

All 5 tests pass with coverage of happy path and error cases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ationToken propagation

- Add PersistEventsAsync(CT) to IEntityEventTracker for outbox phase-1 write
- Add CancellationToken to EmitTransactionalEventsAsync signature
- Implement PersistEventsAsync as no-op in InMemoryEntityEventTracker
- Pass CancellationToken through to IEventRouter.RouteEventsAsync
- Refactor UnitOfWork.CommitAsync to three-phase: persist events, commit tx, dispatch events
- Add Microsoft.Extensions.Hosting.Abstractions package refs to RCommon.Persistence.csproj
- Update all test mocks/verifies to match new interface signatures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements OutboxEventRouter as the transactional outbox IEventRouter: buffers events in-memory via AddTransactionalEvent(s), persists them as OutboxMessage rows via PersistBufferedEventsAsync (Phase 1, within active transaction), and dispatches pending messages via RouteEventsAsync (Phase 3, post-commit). Includes 6 unit tests covering buffering, persistence, field correctness, dispatch, and failure marking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…istence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ad-letter support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… concurrency tests

Implements the AddOutbox<TOutboxStore>() extension on IPersistenceBuilder that wires
all outbox services (store, serializer, router, entity tracker, background processor)
into the DI container with correct lifetimes. Adds UnitOfWork integration test verifying
the two-phase PersistEventsAsync flow through OutboxEntityEventTracker, plus edge-case
concurrency tests for empty buffers, no-pending routing, and dead-letter exclusion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…, and SQLite tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…lter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduces RCommon.MassTransit.Outbox project with IMassTransitOutboxBuilder,
MassTransitOutboxBuilder, and AddOutbox<TDbContext> extension method that wraps
MassTransit's IEntityFrameworkOutboxConfigurator into RCommon's builder pattern.
Includes test project with 4 passing unit tests covering UseSqlServer, UsePostgres,
UseBusOutbox delegation, and null-guard constructor validation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wraps Wolverine's WolverineFx.EntityFrameworkCore durable messaging with
RCommon's builder pattern via IWolverineOutboxBuilder, WolverineOutboxBuilder,
and WolverineOutboxBuilderExtensions. Includes test project with 8 passing
unit tests achieving 100% line/branch/method coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jasonmwebb-lv and others added 20 commits March 23, 2026 07:53
- Add CleanupInterval option (default 1h) to avoid running DELETE queries
  on every polling cycle
- Add clear comment explaining why EFCoreOutboxStore uses client-side
  OrderBy (DateTimeOffset not supported in ORDER BY by all providers)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The RepositoryQuery getter always replaced _repositoryQuery with
_includableQueryable, discarding any ThenInclude results. Fixed by
nulling _includableQueryable after ThenInclude consumes it, and
having Include chain from _repositoryQuery when _includableQueryable
is null (preserving prior ThenInclude chains).

Affects both EFCoreRepository and EFCoreAggregateRepository.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers exponential backoff, distributed locking (SQL Server + PostgreSQL),
dead letter replay, and inbox/idempotency with standalone and auto-check modes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use CTE instead of UPDATE TOP for ordered claims
- Add UPDLOCK, ROWLOCK, READPAST hints for concurrent safety
- Clarify ConfigureAwait(false) requirement in pseudocode
- Fix inbox ConsumerType nullability description
- Add missing Dapper/Linq2Db builder extension files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Section 4.4 documenting OutboxEventRouter V2 behavior
- RouteEventsAsync() no longer reads from store; dispatches retained events
- Failures left for background processor (no MarkFailedAsync call)
- Add missing test files to Section 8

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add implementation sketch for OutboxEventRouter retained-event pattern
- Fix Mode 1 inbox example: use domain-specific ID, not @event.Id
- Clarify that ISerializableEvent has no Id property
- Document that Mode 1 requires consumer-chosen deduplication key

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
12-task plan covering backoff strategy, interface changes, inbox abstractions,
OutboxEventRouter V2, OutboxProcessingService V2, EF Core/Dapper/Linq2Db
store implementations, inbox stores, and full verification.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…Options

Introduces IBackoffStrategy interface and ExponentialBackoffStrategy implementation
for computing retry delays with configurable base delay, max delay, and multiplier.
Extends OutboxOptions with V2 properties: LockDuration, BackoffBaseDelay,
BackoffMaxDelay, BackoffMultiplier, and InboxTableName.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…r replay

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add ClaimAsync with optimistic LINQ fallback (works with SQLite in tests)
- Remove GetPendingAsync, replaced by ClaimAsync with locking semantics
- Update MarkFailedAsync to accept nextRetryAtUtc, clear lock fields
- Add GetDeadLettersAsync with offset/batchSize pagination
- Add ReplayDeadLetterAsync to reset all fields on dead-lettered messages
- Update SaveAsync to copy NextRetryAtUtc, LockedByInstanceId, LockedUntilUtc
- Add _tableName field from OutboxOptions
- Update OutboxMessageConfiguration with new columns and two new indexes
- Rewrite EFCoreOutboxStoreTests with 12 tests covering V2 behaviors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds EFCoreInboxStore (IInboxStore), InboxMessageConfiguration (composite PK on MessageId+ConsumerType), AddInboxMessages ModelBuilder extension, and 4 passing tests. TestOutboxDbContext updated to include inbox schema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add ILockStatementProvider to constructor with null guard
- Remove GetPendingAsync; add ClaimAsync with PostgreSQL/SQL Server raw SQL
- Update SaveAsync to copy NextRetryAtUtc, LockedByInstanceId, LockedUntilUtc
- Update MarkFailedAsync signature to include nextRetryAtUtc and clear locks
- Add GetDeadLettersAsync (LINQ) and ReplayDeadLetterAsync (LINQ update)
- Add UseLockStatementProvider<TProvider> to ILinq2DbPersistenceBuilder and Linq2DbPersistenceBuilder
- Update tests: add lock provider mock and NullLockStatementProvider test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jasonmwebb-lv jasonmwebb-lv merged commit 705e9b5 into main Mar 24, 2026
1 check passed
@jasonmwebb-lv jasonmwebb-lv deleted the feature/ddd branch March 24, 2026 01:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant