using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants; using ColaFlow.Modules.Identity.Domain.Aggregates.Users; using ColaFlow.Modules.Identity.Domain.Aggregates.Invitations; using ColaFlow.Modules.Identity.Domain.Entities; using ColaFlow.Modules.Identity.Infrastructure.Services; using ColaFlow.Shared.Kernel.Common; using MediatR; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Diagnostics; namespace ColaFlow.Modules.Identity.Infrastructure.Persistence; public class IdentityDbContext( DbContextOptions options, ITenantContext tenantContext, IMediator mediator, IHostEnvironment environment, ILogger logger) : DbContext(options) { public DbSet Tenants => Set(); public DbSet Users => Set(); public DbSet RefreshTokens => Set(); public DbSet UserTenantRoles => Set(); public DbSet EmailVerificationTokens => Set(); public DbSet PasswordResetTokens => Set(); public DbSet Invitations => Set(); public DbSet EmailRateLimits => Set(); protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); // Enable query logging in development for performance analysis if (environment.IsDevelopment()) { optionsBuilder .EnableSensitiveDataLogging() .LogTo(Console.WriteLine, LogLevel.Information) .EnableDetailedErrors(); } } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); // Apply all configurations from assembly modelBuilder.ApplyConfigurationsFromAssembly(typeof(IdentityDbContext).Assembly); // Configure Global Query Filters (automatic tenant data filtering) ConfigureGlobalQueryFilters(modelBuilder); } private void ConfigureGlobalQueryFilters(ModelBuilder modelBuilder) { // User entity global query filter // Automatically adds: WHERE tenant_id = @current_tenant_id modelBuilder.Entity().HasQueryFilter(u => !tenantContext.IsSet || u.TenantId == tenantContext.TenantId); // Tenant entity doesn't need filter (need to query all tenants) } /// /// Disable Query Filter (for admin operations) /// public IQueryable WithoutTenantFilter() where T : class { return Set().IgnoreQueryFilters(); } /// /// Override SaveChangesAsync to dispatch domain events before saving /// public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) { var stopwatch = Stopwatch.StartNew(); // Dispatch domain events BEFORE saving changes await DispatchDomainEventsAsync(cancellationToken); // Save changes to database var result = await base.SaveChangesAsync(cancellationToken); stopwatch.Stop(); // Log slow database operations (> 1 second) if (stopwatch.ElapsedMilliseconds > 1000) { logger.LogWarning( "Slow database operation detected: SaveChangesAsync took {ElapsedMs}ms", stopwatch.ElapsedMilliseconds); } return result; } /// /// 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); } } }