Unit of Work & Domain Event Dispatch
Relevant source files
The following files were used as context for generating this wiki page:
- BuildingBlocks/BuildingBlocks.Infrastructure.EfCore/EfCoreUnitOfWork.cs
- BuildingBlocks/BuildingBlocks.Application/Interfaces/IUnitOfWork.cs
- BuildingBlocks/BuildingBlocks.Tests/EfCoreUnitOfWorkTests.cs
- BuildingBlocks/BuildingBlocks.Tests/TransactionBehaviorTests.cs
- Core/Scheduling/Scheduling.Domain.Tests/ApplicationTests/EventHandlerTests/PatientCreatedEventHandlerTests.cs
- claude.md
The Unit of Work (UoW) pattern is a central architectural component in this project, designed to maintain a list of objects affected by a business transaction and coordinate the writing out of changes. Its primary purpose is to ensure atomicity and consistency across database operations while managing the lifecycle of both Domain and Integration events within a single transaction boundary.
This system integrates tightly with MediatR for in-process Domain Event dispatching and leverages an outbox pattern for cross-context communication via Integration Events. By centralizing these concerns, the project enforces a clear separation between domain logic and infrastructure persistence, ensuring that side effects only occur when the primary transaction is successfully committed.
Sources: claude.md:27-37, BuildingBlocks.Infrastructure.EfCore/EfCoreUnitOfWork.cs:13-20
Architecture and Core Components
The implementation centers around the EfCoreUnitOfWork<TContext>, which implements the IUnitOfWork interface. It utilizes Entity Framework Core's DbContext and its ChangeTracker to identify state changes and associated events.
Key Components
| Component | Responsibility |
|---|---|
IUnitOfWork | Interface defining methods for managing transactions, saving changes, and queuing events. |
EfCoreUnitOfWork<TContext> | EF Core implementation of UoW; manages DbContext transactions and event orchestration. |
TransactionBehavior | A MediatR pipeline behavior that automatically wraps Commands in a UoW transaction. |
IHasDomainEvents | An interface implemented by entities that can hold and clear domain events. |
IEventBus | Infrastructure used to publish integration events to an outbox for later delivery. |
Sources: BuildingBlocks.Application/Interfaces/IUnitOfWork.cs:7-24, BuildingBlocks.Infrastructure.EfCore/EfCoreUnitOfWork.cs:13-35, BuildingBlocks.Tests/TransactionBehaviorTests.cs:25-35
Class Relationship Diagram
The following diagram illustrates the relationship between the Unit of Work and the messaging/persistence infrastructure.
Transaction Management
The system supports nested transactions using a depth counter (_transactionDepth). This allows multiple commands or service calls to participate in a single logical transaction without prematurely committing or rolling back.
- BeginTransactionAsync: Increments the depth counter. A real database transaction is only started if depth is 1.
- CloseTransactionAsync: Decrements the depth counter. The actual
CommitorRollbackonly occurs when the depth reaches 0. - Exception Handling: If an exception occurs, the transaction is rolled back, and the queued integration events are cleared to prevent inconsistent side effects.
Sources: BuildingBlocks.Infrastructure.EfCore/EfCoreUnitOfWork.cs:100-149, BuildingBlocks.Tests/EfCoreUnitOfWorkTests.cs:60-120
Transaction Flow
The diagram below shows how TransactionBehavior coordinates with the Unit of Work during a MediatR command execution.
Domain Event Dispatching
Domain events are dispatched automatically during the SaveChangesAsync call, specifically before the data is persisted to the database. This allows domain event handlers to modify the state of other entities within the same transaction.
- Discovery: The
EfCoreUnitOfWorkscans the EFChangeTrackerfor any entities implementingIHasDomainEventsthat contain events. - Dispatch: Events are published using
IMediator.Publish. - Clearing: Events are cleared from entities before publishing to prevent infinite loops if a handler triggers another save.
Sources: BuildingBlocks.Infrastructure.EfCore/EfCoreUnitOfWork.cs:44-45, BuildingBlocks.Infrastructure.EfCore/EfCoreUnitOfWork.cs:64-90
Integration Event Outbox
Integration events facilitate cross-bounded-context communication. Unlike domain events, they are published to an outbox during the SaveChangesAsync process.
- Queueing: Application services or domain event handlers call
QueueIntegrationEventto add events to an internal list. - Outbox Write: During
SaveChangesAsync, the Unit of Work iterates through these events and publishes them via theIEventBus. - Persistence: The
IEventBus(implemented via MassTransit or Wolverine) writes these messages to an Outbox table in the same database transaction. - Delivery: The messages are delivered to the message broker (e.g., RabbitMQ) by a background service only after the transaction is committed.
Sources: BuildingBlocks.Infrastructure.EfCore/EfCoreUnitOfWork.cs:51-56, BuildingBlocks.Infrastructure.EfCore/EfCoreUnitOfWork.cs:92-118, Scheduling.Domain.Tests/ApplicationTests/EventHandlerTests/PatientCreatedEventHandlerTests.cs:33-46
Event Processing Order
| Phase | Action | Timing |
|---|---|---|
| Domain Dispatch | IMediator.Publish(domainEvent) | Before DbContext.SaveChangesAsync |
| Outbox Write | IEventBus.PublishAsync(integrationEvent) | Before DbContext.SaveChangesAsync |
| Database Save | DbContext.SaveChangesAsync | Final persistence of data and outbox messages |
| Transaction Commit | IDbContextTransaction.CommitAsync | Only at the outermost transaction level |
Sources: BuildingBlocks.Infrastructure.EfCore/EfCoreUnitOfWork.cs:41-58
Implementation Example: SaveChangesAsync
The following snippet demonstrates the orchestration logic within the Unit of Work:
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
// 1. Dispatch domain events BEFORE saving (allows handlers to modify state)
await DispatchDomainEventsAsync(cancellationToken);
// 2. Publish integration events to the outbox BEFORE saving.
// This writes to the OutboxMessage table within the transaction.
await PublishIntegrationEventsToOutboxAsync(cancellationToken);
// 3. Persist all changes (Domain state + Outbox entries)
return await _context.SaveChangesAsync(cancellationToken);
}
Sources: BuildingBlocks.Infrastructure.EfCore/EfCoreUnitOfWork.cs:41-58
Conclusion
The Unit of Work and Domain Event Dispatch system provides a robust foundation for maintaining data integrity and implementing Event-Driven Architecture. By leveraging EF Core transactions and MediatR, it ensures that domain side effects are processed within the business transaction, while the Outbox pattern guarantees reliable asynchronous communication between microservices.