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>
951 lines
30 KiB
Markdown
951 lines
30 KiB
Markdown
# 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 `SaveChangesAsync` directly, bypassing event dispatching
|
|
|
|
### Critical Finding
|
|
**Domain events are being collected but never published!** This means:
|
|
- Events like `TenantCreated`, `UserCreated`, `UserRoleAssigned` are 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`**
|
|
```csharp
|
|
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`**
|
|
```csharp
|
|
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:**
|
|
```csharp
|
|
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:**
|
|
```csharp
|
|
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`**
|
|
```csharp
|
|
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 `SaveChangesAsync` override
|
|
- ❌ No domain event dispatching
|
|
- ❌ No UnitOfWork pattern
|
|
|
|
**Repositories (TenantRepository, UserRepository, etc.)**
|
|
```csharp
|
|
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 `SaveChangesAsync` in each module's DbContext
|
|
- ⚠️ Tight coupling to EF Core
|
|
|
|
**Implementation:**
|
|
```csharp
|
|
// 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:**
|
|
```csharp
|
|
// 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:**
|
|
1. Domain events inherit from `DomainEvent` (which is a record)
|
|
2. Event handlers implement `INotificationHandler<TEvent>`
|
|
3. `_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:**
|
|
```csharp
|
|
// 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:**
|
|
```csharp
|
|
// 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)**
|
|
1. Override `SaveChangesAsync` in `IdentityDbContext`
|
|
2. Implement `DispatchDomainEventsAsync` method
|
|
3. Inject `IMediator` into DbContext
|
|
4. Test that events are being published (add logging)
|
|
|
|
**Phase 2: Critical Event Handlers (Day 6-7, ~2 hours)**
|
|
1. `UserCreatedEventHandler` - Audit logging
|
|
2. `TenantCreatedEventHandler` - Audit logging
|
|
3. `UserRoleAssignedEventHandler` - Audit logging + cache invalidation
|
|
|
|
**Phase 3: Additional Event Handlers (Day 7-8, ~2 hours)**
|
|
1. `UserLoggedInEvent` + handler - Login audit trail
|
|
2. `RefreshTokenRevokedEvent` + handler - Security audit
|
|
3. `TenantSuspendedEvent` + handler - Notify users, revoke tokens
|
|
|
|
**Phase 4: Future Events (Day 9+)**
|
|
1. Email verification events
|
|
2. Password reset events
|
|
3. SSO events
|
|
4. 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:**
|
|
```csharp
|
|
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`
|
|
|
|
```csharp
|
|
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`
|
|
|
|
```csharp
|
|
public sealed record UserRemovedFromTenantEvent(
|
|
Guid UserId,
|
|
TenantId TenantId,
|
|
Guid RemovedBy
|
|
) : DomainEvent;
|
|
```
|
|
|
|
**File:** `src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Users/Events/UserLoggedInEvent.cs`
|
|
|
|
```csharp
|
|
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`
|
|
|
|
```csharp
|
|
// 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`
|
|
|
|
```csharp
|
|
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:**
|
|
```csharp
|
|
// 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:**
|
|
1. Assign a role to a user via API
|
|
2. Check logs for "User {UserId} assigned role {Role}"
|
|
3. 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)**
|
|
- ✅ `SaveChangesAsync` override 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**
|
|
```csharp
|
|
// Domain/Aggregates/Users/Events/UserRoleAssignedEvent.cs
|
|
public sealed record UserRoleAssignedEvent(
|
|
Guid UserId,
|
|
TenantId TenantId,
|
|
TenantRole Role,
|
|
Guid AssignedBy
|
|
) : DomainEvent;
|
|
```
|
|
|
|
**2. Raise Event in Aggregate**
|
|
```csharp
|
|
// 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)**
|
|
```csharp
|
|
// 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)**
|
|
```csharp
|
|
// 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)**
|
|
```csharp
|
|
// 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)
|
|
|
|
1. **Implement Domain Event Dispatching**
|
|
- Override `SaveChangesAsync` in `IdentityDbContext`
|
|
- Inject `IMediator` into DbContext
|
|
- Test event dispatching with logging
|
|
|
|
2. **Create Missing Events**
|
|
- `UserRoleAssignedEvent`
|
|
- `UserRemovedFromTenantEvent`
|
|
- `UserLoggedInEvent`
|
|
|
|
3. **Implement Critical Handlers**
|
|
- `UserRoleAssignedEventHandler` (audit logging)
|
|
- `TenantCreatedEventHandler` (audit logging)
|
|
- `UserCreatedEventHandler` (audit logging)
|
|
|
|
### Follow-up Actions (Day 7-8)
|
|
|
|
4. **Expand Event Coverage**
|
|
- Add handlers for all existing 11 domain events
|
|
- Implement audit logging for all major operations
|
|
- Add cache invalidation handlers where needed
|
|
|
|
5. **Testing & Validation**
|
|
- Integration tests for event handling
|
|
- Performance testing (event dispatching overhead)
|
|
- Audit log verification
|
|
|
|
6. **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
|