feat(backend): Implement Domain Events infrastructure and handlers

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>
This commit is contained in:
Yaojia Wang
2025-11-03 20:33:36 +01:00
parent 709068f68b
commit 0e503176c4
13 changed files with 1170 additions and 8 deletions

View File

@@ -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<ITenantContext> _mockTenantContext;
private readonly Mock<IMediator> _mockMediator;
private readonly IdentityDbContext _context;
public GlobalQueryFilterTests()
@@ -20,7 +22,8 @@ public class GlobalQueryFilterTests : IDisposable
.Options;
_mockTenantContext = new Mock<ITenantContext>();
_context = new IdentityDbContext(options, _mockTenantContext.Object);
_mockMediator = new Mock<IMediator>();
_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<IMediator>();
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<IMediator>();
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<IMediator>();
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<IMediator>();
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"));

View File

@@ -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<ITenantContext>();
mockTenantContext.Setup(x => x.IsSet).Returns(false);
_context = new IdentityDbContext(options, mockTenantContext.Object);
var mockMediator = new Mock<IMediator>();
_context = new IdentityDbContext(options, mockTenantContext.Object, mockMediator.Object);
_repository = new TenantRepository(_context);
}