From 0e503176c4348805dfb9fb3c9334119c557b5ad0 Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Mon, 3 Nov 2025 20:33:36 +0100 Subject: [PATCH] feat(backend): Implement Domain Events infrastructure and handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- colaflow-api/DOMAIN-EVENTS-ANALYSIS.md | 950 ++++++++++++++++++ .../TenantCreatedEventHandler.cs | 29 + .../EventHandlers/UserLoggedInEventHandler.cs | 30 + .../UserRemovedFromTenantEventHandler.cs | 30 + .../UserRoleAssignedEventHandler.cs | 32 + .../Users/Events/UserLoggedInEvent.cs | 11 + .../Events/UserRemovedFromTenantEvent.cs | 11 + .../Users/Events/UserRoleAssignedEvent.cs | 12 + .../Persistence/IdentityDbContext.cs | 46 +- .../ColaFlow.Shared.Kernel.csproj | 1 + .../Events/DomainEvent.cs | 4 +- .../Persistence/GlobalQueryFilterTests.cs | 17 +- .../Repositories/TenantRepositoryTests.cs | 5 +- 13 files changed, 1170 insertions(+), 8 deletions(-) create mode 100644 colaflow-api/DOMAIN-EVENTS-ANALYSIS.md create mode 100644 colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/TenantCreatedEventHandler.cs create mode 100644 colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/UserLoggedInEventHandler.cs create mode 100644 colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/UserRemovedFromTenantEventHandler.cs create mode 100644 colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/UserRoleAssignedEventHandler.cs create mode 100644 colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Users/Events/UserLoggedInEvent.cs create mode 100644 colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Users/Events/UserRemovedFromTenantEvent.cs create mode 100644 colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Users/Events/UserRoleAssignedEvent.cs diff --git a/colaflow-api/DOMAIN-EVENTS-ANALYSIS.md b/colaflow-api/DOMAIN-EVENTS-ANALYSIS.md new file mode 100644 index 0000000..918bb29 --- /dev/null +++ b/colaflow-api/DOMAIN-EVENTS-ANALYSIS.md @@ -0,0 +1,950 @@ +# 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 _domainEvents = new(); + + public IReadOnlyCollection 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 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() + .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` 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 options, + ITenantContext tenantContext, + IMediator mediator) // ✅ Inject MediatR + : base(options) + { + _tenantContext = tenantContext; + _mediator = mediator; + } + + public override async Task 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() + .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` +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 +{ + private readonly IAuditLogRepository _auditLogRepository; + private readonly ILogger _logger; + + public UserCreatedEventHandler( + IAuditLogRepository auditLogRepository, + ILogger 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 +{ + 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 options, + ITenantContext tenantContext, + IMediator mediator) // ✅ Add + : base(options) + { + _tenantContext = tenantContext; + _mediator = mediator; // ✅ Add + } + + // ✅ Add SaveChangesAsync override + public override async Task 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() + .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 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 +{ + private readonly ILogger _logger; + + public UserRoleAssignedEventHandler(ILogger 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(s => s.Contains("User") && s.Contains("assigned role")), + It.IsAny()), + 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 +{ + 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 +{ + 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 +{ + 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 diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/TenantCreatedEventHandler.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/TenantCreatedEventHandler.cs new file mode 100644 index 0000000..e031209 --- /dev/null +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/TenantCreatedEventHandler.cs @@ -0,0 +1,29 @@ +using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants.Events; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace ColaFlow.Modules.Identity.Application.EventHandlers; + +public sealed class TenantCreatedEventHandler : INotificationHandler +{ + private readonly ILogger _logger; + + public TenantCreatedEventHandler(ILogger logger) + { + _logger = logger; + } + + public Task Handle(TenantCreatedEvent notification, CancellationToken cancellationToken) + { + _logger.LogInformation( + "Tenant {TenantId} created with slug '{Slug}'", + notification.TenantId, + notification.Slug); + + // Future: Send welcome email + // Future: Initialize default settings + // Future: Create audit log entry + + return Task.CompletedTask; + } +} diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/UserLoggedInEventHandler.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/UserLoggedInEventHandler.cs new file mode 100644 index 0000000..04ee653 --- /dev/null +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/UserLoggedInEventHandler.cs @@ -0,0 +1,30 @@ +using ColaFlow.Modules.Identity.Domain.Aggregates.Users.Events; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace ColaFlow.Modules.Identity.Application.EventHandlers; + +public sealed class UserLoggedInEventHandler : INotificationHandler +{ + private readonly ILogger _logger; + + public UserLoggedInEventHandler(ILogger logger) + { + _logger = logger; + } + + public Task Handle(UserLoggedInEvent notification, CancellationToken cancellationToken) + { + _logger.LogInformation( + "User {UserId} logged in to tenant {TenantId} from IP {IpAddress}", + notification.UserId, + notification.TenantId.Value, + notification.IpAddress); + + // Future: Track login history + // Future: Detect suspicious login patterns + // Future: Update last login timestamp + + return Task.CompletedTask; + } +} diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/UserRemovedFromTenantEventHandler.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/UserRemovedFromTenantEventHandler.cs new file mode 100644 index 0000000..7ce6927 --- /dev/null +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/UserRemovedFromTenantEventHandler.cs @@ -0,0 +1,30 @@ +using ColaFlow.Modules.Identity.Domain.Aggregates.Users.Events; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace ColaFlow.Modules.Identity.Application.EventHandlers; + +public sealed class UserRemovedFromTenantEventHandler : INotificationHandler +{ + private readonly ILogger _logger; + + public UserRemovedFromTenantEventHandler(ILogger logger) + { + _logger = logger; + } + + public Task Handle(UserRemovedFromTenantEvent notification, CancellationToken cancellationToken) + { + _logger.LogInformation( + "User {UserId} removed from tenant {TenantId}. Removed by: {RemovedBy}. Reason: {Reason}", + notification.UserId, + notification.TenantId.Value, + notification.RemovedBy, + notification.Reason); + + // Future: Send notification to user + // Future: Audit log + + return Task.CompletedTask; + } +} diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/UserRoleAssignedEventHandler.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/UserRoleAssignedEventHandler.cs new file mode 100644 index 0000000..30d9af9 --- /dev/null +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Application/EventHandlers/UserRoleAssignedEventHandler.cs @@ -0,0 +1,32 @@ +using ColaFlow.Modules.Identity.Domain.Aggregates.Users.Events; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace ColaFlow.Modules.Identity.Application.EventHandlers; + +public sealed class UserRoleAssignedEventHandler : INotificationHandler +{ + private readonly ILogger _logger; + + public UserRoleAssignedEventHandler(ILogger logger) + { + _logger = logger; + } + + public Task Handle(UserRoleAssignedEvent notification, CancellationToken cancellationToken) + { + _logger.LogInformation( + "User {UserId} assigned role {Role} in tenant {TenantId}. Previous role: {PreviousRole}. Assigned by: {AssignedBy}", + notification.UserId, + notification.Role, + notification.TenantId.Value, + notification.PreviousRole, + notification.AssignedBy); + + // Future: Add audit log to database + // Future: Invalidate user permissions cache + // Future: Send notification to user + + return Task.CompletedTask; + } +} diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Users/Events/UserLoggedInEvent.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Users/Events/UserLoggedInEvent.cs new file mode 100644 index 0000000..87e40ed --- /dev/null +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Users/Events/UserLoggedInEvent.cs @@ -0,0 +1,11 @@ +using ColaFlow.Shared.Kernel.Events; +using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants; + +namespace ColaFlow.Modules.Identity.Domain.Aggregates.Users.Events; + +public sealed record UserLoggedInEvent( + Guid UserId, + TenantId TenantId, + string IpAddress, + string UserAgent +) : DomainEvent; diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Users/Events/UserRemovedFromTenantEvent.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Users/Events/UserRemovedFromTenantEvent.cs new file mode 100644 index 0000000..c77305a --- /dev/null +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Users/Events/UserRemovedFromTenantEvent.cs @@ -0,0 +1,11 @@ +using ColaFlow.Shared.Kernel.Events; +using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants; + +namespace ColaFlow.Modules.Identity.Domain.Aggregates.Users.Events; + +public sealed record UserRemovedFromTenantEvent( + Guid UserId, + TenantId TenantId, + Guid RemovedBy, + string Reason +) : DomainEvent; diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Users/Events/UserRoleAssignedEvent.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Users/Events/UserRoleAssignedEvent.cs new file mode 100644 index 0000000..aa47f72 --- /dev/null +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Domain/Aggregates/Users/Events/UserRoleAssignedEvent.cs @@ -0,0 +1,12 @@ +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, + TenantRole? PreviousRole, + Guid AssignedBy +) : DomainEvent; diff --git a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Persistence/IdentityDbContext.cs b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Persistence/IdentityDbContext.cs index dd3f55a..71f8ab7 100644 --- a/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Persistence/IdentityDbContext.cs +++ b/colaflow-api/src/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure/Persistence/IdentityDbContext.cs @@ -1,6 +1,8 @@ using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants; using ColaFlow.Modules.Identity.Domain.Aggregates.Users; using ColaFlow.Modules.Identity.Infrastructure.Services; +using ColaFlow.Shared.Kernel.Common; +using MediatR; using Microsoft.EntityFrameworkCore; namespace ColaFlow.Modules.Identity.Infrastructure.Persistence; @@ -8,13 +10,16 @@ namespace ColaFlow.Modules.Identity.Infrastructure.Persistence; public class IdentityDbContext : DbContext { private readonly ITenantContext _tenantContext; + private readonly IMediator _mediator; public IdentityDbContext( DbContextOptions options, - ITenantContext tenantContext) + ITenantContext tenantContext, + IMediator mediator) : base(options) { _tenantContext = tenantContext; + _mediator = mediator; } public DbSet Tenants => Set(); @@ -50,4 +55,43 @@ public class IdentityDbContext : DbContext { return Set().IgnoreQueryFilters(); } + + /// + /// Override SaveChangesAsync to dispatch domain events before saving + /// + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + // Dispatch domain events BEFORE saving changes + await DispatchDomainEventsAsync(cancellationToken); + + // Save changes to database + return await base.SaveChangesAsync(cancellationToken); + } + + /// + /// Dispatch domain events to handlers via MediatR + /// + private async Task DispatchDomainEventsAsync(CancellationToken cancellationToken) + { + // Get all aggregate roots with pending domain events + var domainEntities = ChangeTracker + .Entries() + .Where(x => x.Entity.DomainEvents.Any()) + .Select(x => x.Entity) + .ToList(); + + // Collect all domain events + var domainEvents = domainEntities + .SelectMany(x => x.DomainEvents) + .ToList(); + + // Clear events from aggregates (prevent double-publishing) + domainEntities.ForEach(entity => entity.ClearDomainEvents()); + + // Publish each event via MediatR + foreach (var domainEvent in domainEvents) + { + await _mediator.Publish(domainEvent, cancellationToken); + } + } } diff --git a/colaflow-api/src/Shared/ColaFlow.Shared.Kernel/ColaFlow.Shared.Kernel.csproj b/colaflow-api/src/Shared/ColaFlow.Shared.Kernel/ColaFlow.Shared.Kernel.csproj index c8d3251..9f51d03 100644 --- a/colaflow-api/src/Shared/ColaFlow.Shared.Kernel/ColaFlow.Shared.Kernel.csproj +++ b/colaflow-api/src/Shared/ColaFlow.Shared.Kernel/ColaFlow.Shared.Kernel.csproj @@ -7,6 +7,7 @@ + diff --git a/colaflow-api/src/Shared/ColaFlow.Shared.Kernel/Events/DomainEvent.cs b/colaflow-api/src/Shared/ColaFlow.Shared.Kernel/Events/DomainEvent.cs index 02b03e8..9be788b 100644 --- a/colaflow-api/src/Shared/ColaFlow.Shared.Kernel/Events/DomainEvent.cs +++ b/colaflow-api/src/Shared/ColaFlow.Shared.Kernel/Events/DomainEvent.cs @@ -1,9 +1,11 @@ +using MediatR; + namespace ColaFlow.Shared.Kernel.Events; /// /// Base class for all domain events /// -public abstract record DomainEvent +public abstract record DomainEvent : INotification { public Guid EventId { get; init; } = Guid.NewGuid(); public DateTime OccurredOn { get; init; } = DateTime.UtcNow; diff --git a/colaflow-api/tests/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure.Tests/Persistence/GlobalQueryFilterTests.cs b/colaflow-api/tests/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure.Tests/Persistence/GlobalQueryFilterTests.cs index e3d269e..1fea702 100644 --- a/colaflow-api/tests/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure.Tests/Persistence/GlobalQueryFilterTests.cs +++ b/colaflow-api/tests/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure.Tests/Persistence/GlobalQueryFilterTests.cs @@ -3,6 +3,7 @@ using ColaFlow.Modules.Identity.Domain.Aggregates.Users; using ColaFlow.Modules.Identity.Infrastructure.Persistence; using ColaFlow.Modules.Identity.Infrastructure.Services; using FluentAssertions; +using MediatR; using Microsoft.EntityFrameworkCore; using Moq; @@ -11,6 +12,7 @@ namespace ColaFlow.Modules.Identity.Infrastructure.Tests.Persistence; public class GlobalQueryFilterTests : IDisposable { private readonly Mock _mockTenantContext; + private readonly Mock _mockMediator; private readonly IdentityDbContext _context; public GlobalQueryFilterTests() @@ -20,7 +22,8 @@ public class GlobalQueryFilterTests : IDisposable .Options; _mockTenantContext = new Mock(); - _context = new IdentityDbContext(options, _mockTenantContext.Object); + _mockMediator = new Mock(); + _context = new IdentityDbContext(options, _mockTenantContext.Object, _mockMediator.Object); } [Fact] @@ -39,7 +42,8 @@ public class GlobalQueryFilterTests : IDisposable .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; - using var context = new IdentityDbContext(options, mockTenantContext.Object); + var mockMediator = new Mock(); + using var context = new IdentityDbContext(options, mockTenantContext.Object, mockMediator.Object); var user1 = User.CreateLocal( tenant1Id, @@ -81,7 +85,8 @@ public class GlobalQueryFilterTests : IDisposable .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; - using var context = new IdentityDbContext(options, mockTenantContext.Object); + var mockMediator = new Mock(); + using var context = new IdentityDbContext(options, mockTenantContext.Object, mockMediator.Object); var user1 = User.CreateLocal(tenant1Id, Email.Create("admin@tenant1.com"), "pass", FullName.Create("Admin One")); var user2 = User.CreateLocal(tenant2Id, Email.Create("admin@tenant2.com"), "pass", FullName.Create("Admin Two")); @@ -111,7 +116,8 @@ public class GlobalQueryFilterTests : IDisposable .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; - using var context = new IdentityDbContext(options, mockTenantContext.Object); + var mockMediator = new Mock(); + using var context = new IdentityDbContext(options, mockTenantContext.Object, mockMediator.Object); var user1 = User.CreateLocal(tenant1Id, Email.Create("user1@test.com"), "pass", FullName.Create("User One")); var user2 = User.CreateLocal(tenant2Id, Email.Create("user2@test.com"), "pass", FullName.Create("User Two")); @@ -142,7 +148,8 @@ public class GlobalQueryFilterTests : IDisposable .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) .Options; - using var context = new IdentityDbContext(options, mockTenantContext.Object); + var mockMediator = new Mock(); + using var context = new IdentityDbContext(options, mockTenantContext.Object, mockMediator.Object); var user1 = User.CreateLocal(tenant1Id, Email.Create("john@tenant1.com"), "pass", FullName.Create("John Doe")); var user2 = User.CreateLocal(tenant2Id, Email.Create("jane@tenant2.com"), "pass", FullName.Create("Jane Doe")); diff --git a/colaflow-api/tests/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure.Tests/Repositories/TenantRepositoryTests.cs b/colaflow-api/tests/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure.Tests/Repositories/TenantRepositoryTests.cs index 77c4b50..5a5639e 100644 --- a/colaflow-api/tests/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure.Tests/Repositories/TenantRepositoryTests.cs +++ b/colaflow-api/tests/Modules/Identity/ColaFlow.Modules.Identity.Infrastructure.Tests/Repositories/TenantRepositoryTests.cs @@ -3,6 +3,7 @@ using ColaFlow.Modules.Identity.Infrastructure.Persistence; using ColaFlow.Modules.Identity.Infrastructure.Persistence.Repositories; using ColaFlow.Modules.Identity.Infrastructure.Services; using FluentAssertions; +using MediatR; using Microsoft.EntityFrameworkCore; using Moq; @@ -22,7 +23,9 @@ public class TenantRepositoryTests : IDisposable var mockTenantContext = new Mock(); mockTenantContext.Setup(x => x.IsSet).Returns(false); - _context = new IdentityDbContext(options, mockTenantContext.Object); + var mockMediator = new Mock(); + + _context = new IdentityDbContext(options, mockTenantContext.Object, mockMediator.Object); _repository = new TenantRepository(_context); }