Add complete domain events dispatching infrastructure and critical event handlers for Identity module. Changes: - Added IMediator injection to IdentityDbContext - Implemented SaveChangesAsync override to dispatch domain events before persisting - Made DomainEvent base class implement INotification (added MediatR.Contracts dependency) - Created 3 new domain events: UserRoleAssignedEvent, UserRemovedFromTenantEvent, UserLoggedInEvent - Implemented 4 event handlers with structured logging: - UserRoleAssignedEventHandler (audit log, cache invalidation placeholder) - UserRemovedFromTenantEventHandler (notification placeholder) - UserLoggedInEventHandler (login tracking placeholder) - TenantCreatedEventHandler (welcome email placeholder) - Updated unit tests to inject mock IMediator into IdentityDbContext Technical Details: - Domain events are now published via MediatR within the same transaction - Events are dispatched BEFORE SaveChangesAsync to ensure atomicity - Event handlers auto-registered by MediatR assembly scanning - All handlers include structured logging for observability Next Steps (Phase 3): - Update command handlers to raise new events (UserLoggedInEvent, UserRoleAssignedEvent) - Add event raising logic to User/Tenant aggregates - Implement audit logging persistence (currently just logging) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
30 KiB
Domain Events Implementation Analysis & Plan
Date: 2025-11-03 Module: Identity Module (ColaFlow.Modules.Identity) Status: Gap Analysis Complete - Implementation Required
Executive Summary
Current State
The Identity module has partial domain events implementation:
- ✅ Domain event infrastructure exists (base classes, AggregateRoot pattern)
- ✅ 11 domain events defined in the domain layer
- ✅ Domain events are being raised in aggregates (Tenant, User)
- ❌ Domain events are NOT being dispatched (events are raised but never published)
- ❌ No domain event handlers implemented
- ❌ Repository pattern calls
SaveChangesAsyncdirectly, bypassing event dispatching
Critical Finding
Domain events are being collected but never published! This means:
- Events like
TenantCreated,UserCreated,UserRoleAssignedare raised but silently discarded - No audit logging, no side effects, no cross-module notifications
- The infrastructure is 80% complete but missing the final critical piece
Recommended Action
Immediate implementation required - Domain events are foundational for:
- Audit logging (required for compliance)
- Cross-module communication (required for modularity)
- Side effects (email notifications, cache invalidation, etc.)
- Event sourcing (future requirement)
1. Current State Assessment
1.1 Domain Event Infrastructure (✅ Complete)
Base Classes
ColaFlow.Shared.Kernel.Events.DomainEvent
public abstract record DomainEvent
{
public Guid EventId { get; init; } = Guid.NewGuid();
public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
}
- ✅ Properly designed as record (immutable)
- ✅ Auto-generates EventId and timestamp
- ✅ Follows best practices
ColaFlow.Shared.Kernel.Common.AggregateRoot
public abstract class AggregateRoot : Entity
{
private readonly List<DomainEvent> _domainEvents = new();
public IReadOnlyCollection<DomainEvent> DomainEvents => _domainEvents.AsReadOnly();
protected void AddDomainEvent(DomainEvent domainEvent)
{
_domainEvents.Add(domainEvent);
}
public void ClearDomainEvents()
{
_domainEvents.Clear();
}
}
- ✅ Encapsulates domain events collection
- ✅ Provides AddDomainEvent method for aggregates
- ✅ Provides ClearDomainEvents for cleanup after dispatching
- ✅ Follows DDD best practices (encapsulation)
1.2 Domain Events Defined (✅ Complete)
Tenant Events (7 events)
| Event | File | Raised In | Purpose |
|---|---|---|---|
TenantCreatedEvent |
Tenants/Events/ |
Tenant.Create() |
New tenant registration |
TenantActivatedEvent |
Tenants/Events/ |
Tenant.Activate() |
Tenant reactivation |
TenantSuspendedEvent |
Tenants/Events/ |
Tenant.Suspend() |
Tenant suspension |
TenantCancelledEvent |
Tenants/Events/ |
Tenant.Cancel() |
Tenant cancellation |
TenantPlanUpgradedEvent |
Tenants/Events/ |
Tenant.UpgradePlan() |
Plan upgrade |
SsoConfiguredEvent |
Tenants/Events/ |
Tenant.ConfigureSso() |
SSO setup |
SsoDisabledEvent |
Tenants/Events/ |
Tenant.DisableSso() |
SSO removal |
Example:
public sealed record TenantCreatedEvent(Guid TenantId, string Slug) : DomainEvent;
User Events (4 events)
| Event | File | Raised In | Purpose |
|---|---|---|---|
UserCreatedEvent |
Users/Events/ |
User.CreateLocal() |
Local user registration |
UserCreatedFromSsoEvent |
Users/Events/ |
User.CreateFromSso() |
SSO user registration |
UserPasswordChangedEvent |
Users/Events/ |
User.UpdatePassword() |
Password change |
UserSuspendedEvent |
Users/Events/ |
User.Suspend() |
User suspension |
Example:
public sealed record UserCreatedEvent(
Guid UserId,
string Email,
TenantId TenantId
) : DomainEvent;
1.3 Event Dispatching Infrastructure (❌ Missing in Identity Module)
ProjectManagement Module (Reference Implementation)
ColaFlow.Modules.ProjectManagement.Infrastructure.Persistence.UnitOfWork
public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
// Dispatch domain events before saving
await DispatchDomainEventsAsync(cancellationToken);
// Save changes to database
return await _context.SaveChangesAsync(cancellationToken);
}
private async Task DispatchDomainEventsAsync(CancellationToken cancellationToken)
{
// Get all entities with domain events
var domainEntities = _context.ChangeTracker
.Entries<AggregateRoot>()
.Where(x => x.Entity.DomainEvents.Any())
.Select(x => x.Entity)
.ToList();
// Get all domain events
var domainEvents = domainEntities
.SelectMany(x => x.DomainEvents)
.ToList();
// Clear domain events from entities
domainEntities.ForEach(entity => entity.ClearDomainEvents());
// TODO: Dispatch domain events to handlers
// This will be implemented when we add MediatR
await Task.CompletedTask;
}
Status: ✅ Infrastructure exists in ProjectManagement module, ❌ Not implemented in Identity module
Identity Module (Current Implementation)
IdentityDbContext
- ❌ No
SaveChangesAsyncoverride - ❌ No domain event dispatching
- ❌ No UnitOfWork pattern
Repositories (TenantRepository, UserRepository, etc.)
public async Task AddAsync(Tenant tenant, CancellationToken cancellationToken = default)
{
await _context.Tenants.AddAsync(tenant, cancellationToken);
await _context.SaveChangesAsync(cancellationToken); // ❌ Direct call, bypasses events
}
Problem: Repositories call DbContext.SaveChangesAsync() directly, so domain events are never dispatched.
1.4 Domain Event Handlers (❌ Missing)
Current State:
- ❌ No
INotificationHandler<TEvent>implementations - ❌ No event handler folder structure
- ❌ MediatR registered in Application layer but not configured for domain events
Expected Structure (Not Present):
ColaFlow.Modules.Identity.Application/
├── EventHandlers/
│ ├── Tenants/
│ │ ├── TenantCreatedEventHandler.cs ❌ Missing
│ │ └── TenantPlanUpgradedEventHandler.cs ❌ Missing
│ └── Users/
│ ├── UserCreatedEventHandler.cs ❌ Missing
│ └── UserSuspendedEventHandler.cs ❌ Missing
2. Gap Analysis
2.1 What's Working
| Component | Status | Notes |
|---|---|---|
| Domain Event Base Class | ✅ Complete | Well-designed record with EventId and timestamp |
| AggregateRoot Pattern | ✅ Complete | Proper encapsulation of domain events collection |
| Domain Events Defined | ✅ Complete | 11 events defined and raised in aggregates |
| MediatR Registration | ✅ Complete | MediatR registered in Application layer |
2.2 What's Missing
| Component | Status | Impact | Priority |
|---|---|---|---|
| Event Dispatching in DbContext | ❌ Missing | HIGH - Events never published | CRITICAL |
| UnitOfWork Pattern | ❌ Missing | HIGH - No transaction boundary for events | CRITICAL |
| Domain Event Handlers | ❌ Missing | HIGH - No side effects, no audit logging | HIGH |
| MediatR Integration for Events | ❌ Missing | HIGH - Events not routed to handlers | CRITICAL |
| Repository Pattern Refactoring | ❌ Missing | MEDIUM - Repositories bypass UnitOfWork | HIGH |
2.3 Missing Events (Day 6+ Features)
Based on Day 4-6 implementation, these events should exist but don't:
| Event | Scenario | Raised In | Priority |
|---|---|---|---|
UserLoggedInEvent |
Login success | LoginCommandHandler | HIGH |
UserLoginFailedEvent |
Login failure | LoginCommandHandler | MEDIUM |
RefreshTokenGeneratedEvent |
Token refresh | RefreshTokenService | MEDIUM |
RefreshTokenRevokedEvent |
Token revocation | RefreshTokenService | MEDIUM |
UserRoleAssignedEvent |
Role assignment | AssignUserRoleCommand | HIGH |
UserRoleUpdatedEvent |
Role change | AssignUserRoleCommand | HIGH |
UserRemovedFromTenantEvent |
User removal | RemoveUserFromTenantCommand | HIGH |
UserTokensRevokedEvent |
Token revocation | RemoveUserFromTenantCommand | MEDIUM |
3. Recommended Architecture
3.1 Domain Event Dispatching Pattern
Option A: Dispatch in DbContext.SaveChangesAsync (Recommended)
Pros:
- ✅ Centralized event dispatching
- ✅ Consistent across all operations
- ✅ Events dispatched within transaction boundary
- ✅ Follows EF Core best practices
Cons:
- ⚠️ Requires overriding
SaveChangesAsyncin each module's DbContext - ⚠️ Tight coupling to EF Core
Implementation:
// IdentityDbContext.cs
public class IdentityDbContext : DbContext
{
private readonly IMediator _mediator;
public IdentityDbContext(
DbContextOptions<IdentityDbContext> options,
ITenantContext tenantContext,
IMediator mediator) // ✅ Inject MediatR
: base(options)
{
_tenantContext = tenantContext;
_mediator = mediator;
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
// Dispatch domain events BEFORE saving
await DispatchDomainEventsAsync(cancellationToken);
// Save changes to database
return await base.SaveChangesAsync(cancellationToken);
}
private async Task DispatchDomainEventsAsync(CancellationToken cancellationToken)
{
// Get all aggregate roots with domain events
var domainEntities = ChangeTracker
.Entries<AggregateRoot>()
.Where(x => x.Entity.DomainEvents.Any())
.Select(x => x.Entity)
.ToList();
// Get all domain events
var domainEvents = domainEntities
.SelectMany(x => x.DomainEvents)
.ToList();
// Clear domain events from entities
domainEntities.ForEach(entity => entity.ClearDomainEvents());
// Dispatch events to handlers via MediatR
foreach (var domainEvent in domainEvents)
{
await _mediator.Publish(domainEvent, cancellationToken);
}
}
}
Option B: Dispatch in UnitOfWork (Alternative)
Pros:
- ✅ Decouples from DbContext
- ✅ Testable without EF Core
- ✅ Follows Clean Architecture more strictly
Cons:
- ⚠️ Requires UnitOfWork pattern implementation
- ⚠️ More boilerplate code
- ⚠️ Repositories must use UnitOfWork instead of direct SaveChangesAsync
Not recommended for now - Option A is simpler and sufficient for current needs.
3.2 MediatR Configuration
Current Configuration:
// Application/DependencyInjection.cs
public static IServiceCollection AddIdentityApplication(this IServiceCollection services)
{
// MediatR
services.AddMediatR(config =>
{
config.RegisterServicesFromAssembly(typeof(DependencyInjection).Assembly);
});
// FluentValidation
services.AddValidatorsFromAssembly(typeof(DependencyInjection).Assembly);
return services;
}
Status: ✅ Already configured for commands/queries, will automatically handle domain events
How MediatR Works:
- Domain events inherit from
DomainEvent(which is a record) - Event handlers implement
INotificationHandler<TEvent> _mediator.Publish(event)dispatches to ALL handlers
Key Point: MediatR treats domain events as notifications (pub-sub pattern), so multiple handlers can react to the same event.
3.3 Domain Event Handler Pattern
Handler Structure:
// Application/EventHandlers/Users/UserCreatedEventHandler.cs
public class UserCreatedEventHandler : INotificationHandler<UserCreatedEvent>
{
private readonly IAuditLogRepository _auditLogRepository;
private readonly ILogger<UserCreatedEventHandler> _logger;
public UserCreatedEventHandler(
IAuditLogRepository auditLogRepository,
ILogger<UserCreatedEventHandler> logger)
{
_auditLogRepository = auditLogRepository;
_logger = logger;
}
public async Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation(
"User {UserId} created in tenant {TenantId}",
notification.UserId,
notification.TenantId);
// Example: Log to audit trail
var auditLog = AuditLog.Create(
entityType: "User",
entityId: notification.UserId,
action: "Created",
performedBy: notification.UserId, // Self-registration
timestamp: notification.OccurredOn);
await _auditLogRepository.AddAsync(auditLog, cancellationToken);
}
}
Multiple Handlers for Same Event:
// Application/EventHandlers/Users/UserCreatedEmailNotificationHandler.cs
public class UserCreatedEmailNotificationHandler : INotificationHandler<UserCreatedEvent>
{
private readonly IEmailService _emailService;
public async Task Handle(UserCreatedEvent notification, CancellationToken cancellationToken)
{
// Send welcome email
await _emailService.SendWelcomeEmailAsync(
notification.Email,
notification.UserId,
cancellationToken);
}
}
Key Benefits:
- ✅ Single Responsibility Principle (each handler does one thing)
- ✅ Decoupled side effects (audit, email, cache, etc.)
- ✅ Easy to add new handlers without modifying existing code
4. Implementation Plan
Option A: Implement Now (Recommended)
Reasoning:
- Domain events are fundamental to the architecture
- Required for Day 6 features (role management audit)
- Critical for audit logging and compliance
- Relatively small implementation effort (2-4 hours)
Timeline: Day 6 (Today) - Implement alongside role management features
Option B: Implement in Day 7
Reasoning:
- Can defer if Day 6 deadline is tight
- Focus on completing role management first
- Implement events as cleanup/refactoring task
Timeline: Day 7 (Tomorrow) - Dedicated domain events implementation day
Option C: Incremental Implementation
Reasoning:
- Implement infrastructure first (dispatching in DbContext)
- Add event handlers incrementally as needed
- Start with critical events (UserCreated, TenantCreated, UserRoleAssigned)
Timeline: Days 6-8 - Spread across multiple days
✅ RECOMMENDED: Option C (Incremental Implementation)
Phase 1: Infrastructure (Day 6, ~1 hour)
- Override
SaveChangesAsyncinIdentityDbContext - Implement
DispatchDomainEventsAsyncmethod - Inject
IMediatorinto DbContext - Test that events are being published (add logging)
Phase 2: Critical Event Handlers (Day 6-7, ~2 hours)
UserCreatedEventHandler- Audit loggingTenantCreatedEventHandler- Audit loggingUserRoleAssignedEventHandler- Audit logging + cache invalidation
Phase 3: Additional Event Handlers (Day 7-8, ~2 hours)
UserLoggedInEvent+ handler - Login audit trailRefreshTokenRevokedEvent+ handler - Security auditTenantSuspendedEvent+ handler - Notify users, revoke tokens
Phase 4: Future Events (Day 9+)
- Email verification events
- Password reset events
- SSO events
- Cross-module integration events
5. Step-by-Step Implementation Guide
Step 1: Add Domain Event Dispatching to DbContext
File: src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Persistence/IdentityDbContext.cs
Changes:
using ColaFlow.Shared.Kernel.Common;
using MediatR;
public class IdentityDbContext : DbContext
{
private readonly ITenantContext _tenantContext;
private readonly IMediator _mediator; // ✅ Add
public IdentityDbContext(
DbContextOptions<IdentityDbContext> options,
ITenantContext tenantContext,
IMediator mediator) // ✅ Add
: base(options)
{
_tenantContext = tenantContext;
_mediator = mediator; // ✅ Add
}
// ✅ Add SaveChangesAsync override
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
await DispatchDomainEventsAsync(cancellationToken);
return await base.SaveChangesAsync(cancellationToken);
}
// ✅ Add DispatchDomainEventsAsync method
private async Task DispatchDomainEventsAsync(CancellationToken cancellationToken)
{
var domainEntities = ChangeTracker
.Entries<AggregateRoot>()
.Where(x => x.Entity.DomainEvents.Any())
.Select(x => x.Entity)
.ToList();
var domainEvents = domainEntities
.SelectMany(x => x.DomainEvents)
.ToList();
domainEntities.ForEach(entity => entity.ClearDomainEvents());
foreach (var domainEvent in domainEvents)
{
await _mediator.Publish(domainEvent, cancellationToken);
}
}
}
Estimated Time: 15 minutes
Step 2: Create Missing Domain Events
File: src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Users/Events/UserRoleAssignedEvent.cs
using ColaFlow.Shared.Kernel.Events;
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
namespace ColaFlow.Modules.Identity.Domain.Aggregates.Users.Events;
public sealed record UserRoleAssignedEvent(
Guid UserId,
TenantId TenantId,
TenantRole Role,
Guid AssignedBy
) : DomainEvent;
File: src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Users/Events/UserRemovedFromTenantEvent.cs
public sealed record UserRemovedFromTenantEvent(
Guid UserId,
TenantId TenantId,
Guid RemovedBy
) : DomainEvent;
File: src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Users/Events/UserLoggedInEvent.cs
public sealed record UserLoggedInEvent(
Guid UserId,
TenantId TenantId,
string IpAddress,
string UserAgent
) : DomainEvent;
Estimated Time: 30 minutes
Step 3: Raise Events in Aggregates
Update: AssignUserRoleCommandHandler to raise UserRoleAssignedEvent
// AssignUserRoleCommandHandler.cs
public async Task<Unit> Handle(AssignUserRoleCommand request, CancellationToken cancellationToken)
{
// ... existing validation logic ...
// Create or update role assignment
var userTenantRole = UserTenantRole.Create(userId, tenantId, request.Role);
await _userTenantRoleRepository.AddOrUpdateAsync(userTenantRole, cancellationToken);
// ✅ Raise domain event (if we make UserTenantRole an AggregateRoot)
// OR raise event from User aggregate
var user = await _userRepository.GetByIdAsync(userId, cancellationToken);
if (user != null)
{
user.AddDomainEvent(new UserRoleAssignedEvent(
userId.Value,
tenantId,
request.Role,
currentUserId)); // From JWT claims
}
return Unit.Value;
}
Estimated Time: 1 hour (refactor command handlers)
Step 4: Create Event Handlers
File: src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/Users/UserRoleAssignedEventHandler.cs
using ColaFlow.Modules.Identity.Domain.Aggregates.Users.Events;
using MediatR;
using Microsoft.Extensions.Logging;
namespace ColaFlow.Modules.Identity.Application.EventHandlers.Users;
public class UserRoleAssignedEventHandler : INotificationHandler<UserRoleAssignedEvent>
{
private readonly ILogger<UserRoleAssignedEventHandler> _logger;
public UserRoleAssignedEventHandler(ILogger<UserRoleAssignedEventHandler> logger)
{
_logger = logger;
}
public Task Handle(UserRoleAssignedEvent notification, CancellationToken cancellationToken)
{
_logger.LogInformation(
"User {UserId} assigned role {Role} in tenant {TenantId} by user {AssignedBy}",
notification.UserId,
notification.Role,
notification.TenantId,
notification.AssignedBy);
// TODO: Add to audit log
// TODO: Invalidate user's cached permissions
// TODO: Send notification to user
return Task.CompletedTask;
}
}
Estimated Time: 30 minutes per handler (create 3-5 handlers)
Step 5: Test Domain Events
Test Script:
// Integration test
[Fact]
public async Task AssignUserRole_Should_Raise_UserRoleAssignedEvent()
{
// Arrange
var command = new AssignUserRoleCommand(userId, tenantId, TenantRole.Admin);
// Act
await _mediator.Send(command);
// Assert
// Verify event was raised and handled
_mockLogger.Verify(
x => x.LogInformation(
It.Is<string>(s => s.Contains("User") && s.Contains("assigned role")),
It.IsAny<object[]>()),
Times.Once);
}
Manual Test:
- Assign a role to a user via API
- Check logs for "User {UserId} assigned role {Role}"
- Verify event was published and handler executed
Estimated Time: 30 minutes
6. Priority Assessment
Critical Events (Implement in Day 6)
| Event | Scenario | Handler Actions | Priority |
|---|---|---|---|
UserRoleAssignedEvent |
Role assignment | Audit log, cache invalidation, notification | CRITICAL |
UserRemovedFromTenantEvent |
User removal | Audit log, revoke tokens, cleanup | CRITICAL |
TenantCreatedEvent |
Tenant registration | Audit log, send welcome email | HIGH |
UserCreatedEvent |
User registration | Audit log, send welcome email | HIGH |
High Priority Events (Implement in Day 7)
| Event | Scenario | Handler Actions | Priority |
|---|---|---|---|
UserLoggedInEvent |
Login success | Audit log, update LastLoginAt | HIGH |
RefreshTokenRevokedEvent |
Token revocation | Audit log, security notification | HIGH |
TenantSuspendedEvent |
Tenant suspension | Notify users, revoke all tokens | HIGH |
UserSuspendedEvent |
User suspension | Revoke tokens, audit log | HIGH |
Medium Priority Events (Implement in Day 8+)
| Event | Scenario | Handler Actions | Priority |
|---|---|---|---|
UserPasswordChangedEvent |
Password change | Audit log, revoke old tokens, email notification | MEDIUM |
TenantPlanUpgradedEvent |
Plan upgrade | Update limits, audit log, send invoice | MEDIUM |
SsoConfiguredEvent |
SSO setup | Audit log, notify admins | MEDIUM |
7. Risks & Mitigation
Risk 1: Performance Impact
Risk: Dispatching many events could slow down SaveChangesAsync Mitigation:
- Domain events are published in-process (fast)
- Consider async background processing for non-critical events (future)
- Monitor performance with logging
Risk 2: Event Handler Failures
Risk: Event handler throws exception, entire transaction rolls back Mitigation:
- Wrap event dispatching in try-catch
- Log exceptions but don't fail transaction
- Consider eventual consistency for non-critical handlers
Risk 3: Event Ordering
Risk: Events might be processed out of order Mitigation:
- Events are dispatched in the order they were raised (in single transaction)
- Use OccurredOn timestamp for ordering if needed
- Consider event sequence numbers (future)
Risk 4: Missing Events
Risk: Forgetting to raise events in new features Mitigation:
- Document event-raising conventions
- Code review checklist
- Integration tests to verify events are raised
8. Success Metrics
Implementation Success Criteria
Phase 1: Infrastructure (Day 6)
- ✅
SaveChangesAsyncoverride implemented in IdentityDbContext - ✅ Domain events are being published (verified via logging)
- ✅ No breaking changes to existing functionality
- ✅ Unit tests pass
Phase 2: Critical Handlers (Day 6-7)
- ✅ 3-5 event handlers implemented and tested
- ✅ Audit logs are being created for critical operations
- ✅ Events are visible in application logs
- ✅ Integration tests verify event handling
Phase 3: Full Coverage (Day 8+)
- ✅ All 15+ events have at least one handler
- ✅ Audit logging complete for all major operations
- ✅ Cross-module events work correctly
- ✅ Performance impact is acceptable (<10ms per event)
9. Example: Complete Event Flow
Scenario: User Role Assignment
1. Domain Event Definition
// Domain/Aggregates/Users/Events/UserRoleAssignedEvent.cs
public sealed record UserRoleAssignedEvent(
Guid UserId,
TenantId TenantId,
TenantRole Role,
Guid AssignedBy
) : DomainEvent;
2. Raise Event in Aggregate
// Domain/Aggregates/Users/User.cs
public class User : AggregateRoot
{
public void AssignRole(TenantRole role, Guid assignedBy)
{
// Business logic validation
if (Status == UserStatus.Deleted)
throw new InvalidOperationException("Cannot assign role to deleted user");
// Raise domain event
AddDomainEvent(new UserRoleAssignedEvent(
Id,
TenantId,
role,
assignedBy));
}
}
3. Event Handler (Audit Logging)
// Application/EventHandlers/Users/UserRoleAssignedAuditHandler.cs
public class UserRoleAssignedAuditHandler : INotificationHandler<UserRoleAssignedEvent>
{
private readonly IAuditLogRepository _auditLogRepository;
public async Task Handle(UserRoleAssignedEvent notification, CancellationToken cancellationToken)
{
var auditLog = AuditLog.Create(
entityType: "User",
entityId: notification.UserId,
action: $"RoleAssigned:{notification.Role}",
performedBy: notification.AssignedBy,
timestamp: notification.OccurredOn,
tenantId: notification.TenantId);
await _auditLogRepository.AddAsync(auditLog, cancellationToken);
}
}
4. Event Handler (Cache Invalidation)
// Application/EventHandlers/Users/UserRoleAssignedCacheHandler.cs
public class UserRoleAssignedCacheHandler : INotificationHandler<UserRoleAssignedEvent>
{
private readonly IDistributedCache _cache;
public async Task Handle(UserRoleAssignedEvent notification, CancellationToken cancellationToken)
{
// Invalidate user's permissions cache
var cacheKey = $"user:permissions:{notification.UserId}";
await _cache.RemoveAsync(cacheKey, cancellationToken);
}
}
5. Event Handler (Notification)
// Application/EventHandlers/Users/UserRoleAssignedNotificationHandler.cs
public class UserRoleAssignedNotificationHandler : INotificationHandler<UserRoleAssignedEvent>
{
private readonly INotificationService _notificationService;
public async Task Handle(UserRoleAssignedEvent notification, CancellationToken cancellationToken)
{
// Send notification to user
await _notificationService.SendAsync(
userId: notification.UserId,
title: "Role Updated",
message: $"Your role has been changed to {notification.Role}",
cancellationToken);
}
}
6. Dispatching Flow
User calls: POST /api/tenants/{tenantId}/users/{userId}/role
→ AssignUserRoleCommandHandler
→ user.AssignRole(role, currentUserId)
→ user.AddDomainEvent(new UserRoleAssignedEvent(...))
→ _userRepository.UpdateAsync(user)
→ _context.SaveChangesAsync()
→ DispatchDomainEventsAsync()
→ _mediator.Publish(UserRoleAssignedEvent)
→ UserRoleAssignedAuditHandler.Handle()
→ UserRoleAssignedCacheHandler.Handle()
→ UserRoleAssignedNotificationHandler.Handle()
→ base.SaveChangesAsync() // Commit transaction
10. Next Steps
Immediate Actions (Day 6)
-
Implement Domain Event Dispatching
- Override
SaveChangesAsyncinIdentityDbContext - Inject
IMediatorinto DbContext - Test event dispatching with logging
- Override
-
Create Missing Events
UserRoleAssignedEventUserRemovedFromTenantEventUserLoggedInEvent
-
Implement Critical Handlers
UserRoleAssignedEventHandler(audit logging)TenantCreatedEventHandler(audit logging)UserCreatedEventHandler(audit logging)
Follow-up Actions (Day 7-8)
-
Expand Event Coverage
- Add handlers for all existing 11 domain events
- Implement audit logging for all major operations
- Add cache invalidation handlers where needed
-
Testing & Validation
- Integration tests for event handling
- Performance testing (event dispatching overhead)
- Audit log verification
-
Documentation
- Update architecture documentation
- Document event-raising conventions
- Create event handler development guide
11. Conclusion
Summary
Current State:
- Domain event infrastructure: 80% complete
- Domain events defined: 11 events (sufficient for Day 1-6)
- Critical gap: Event dispatching not implemented
Recommended Action:
- Implement domain event dispatching in Day 6 (1 hour)
- Add critical event handlers alongside Day 6 features (2 hours)
- Complete event coverage in Day 7-8 (2-4 hours)
Total Effort: 5-7 hours spread across Days 6-8
Value:
- Complete audit trail for compliance
- Foundation for cross-module communication
- Side effects (notifications, cache invalidation)
- Event sourcing ready (future)
Decision
Proceed with Option C (Incremental Implementation)
- Phase 1 (Day 6): Infrastructure + critical handlers
- Phase 2 (Day 7-8): Complete event coverage
- Phase 3 (Day 9+): Advanced features (background processing, event sourcing)
Document Status: ✅ Analysis Complete Recommended Decision: Implement domain events incrementally starting Day 6 Next Review: After Phase 1 implementation Owner: Backend Team Last Updated: 2025-11-03