using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace DiunaBI.Infrastructure.Interceptors; public class EntityChangeInterceptor : SaveChangesInterceptor { private readonly object? _hubContext; private readonly ILogger? _logger; private readonly List<(string Module, string Id, string Operation)> _pendingChanges = new(); public EntityChangeInterceptor(IServiceProvider serviceProvider) { _logger = serviceProvider.GetService(typeof(ILogger)) as ILogger; // Try to get hub context - it may not be registered in some scenarios (e.g., migrations) try { var hubType = Type.GetType("DiunaBI.API.Hubs.EntityChangeHub, DiunaBI.API"); if (hubType != null) { var hubContextType = typeof(IHubContext<>).MakeGenericType(hubType); _hubContext = serviceProvider.GetService(hubContextType); if (_hubContext != null) { _logger?.LogInformation("✅ EntityChangeInterceptor: Hub context initialized"); Console.WriteLine("✅ EntityChangeInterceptor: Hub context initialized"); } else { _logger?.LogWarning("⚠️ EntityChangeInterceptor: Hub context is null"); Console.WriteLine("⚠️ EntityChangeInterceptor: Hub context is null"); } } else { _logger?.LogWarning("⚠️ EntityChangeInterceptor: Hub type not found"); Console.WriteLine("⚠️ EntityChangeInterceptor: Hub type not found"); } } catch (Exception ex) { _logger?.LogError(ex, "❌ EntityChangeInterceptor: Failed to initialize hub context"); Console.WriteLine($"❌ EntityChangeInterceptor: Failed to initialize hub context: {ex.Message}"); _hubContext = null; } } public override ValueTask> SavingChangesAsync( DbContextEventData eventData, InterceptionResult result, CancellationToken cancellationToken = default) { _pendingChanges.Clear(); Console.WriteLine($"🔍 EntityChangeInterceptor.SavingChangesAsync called. HubContext null? {_hubContext == null}, Context null? {eventData.Context == null}"); if (_hubContext != null && eventData.Context != null) { // Capture changes BEFORE save var entries = eventData.Context.ChangeTracker.Entries().ToList(); Console.WriteLine($"🔍 Found {entries.Count} total entries in ChangeTracker"); foreach (var entry in entries) { Console.WriteLine($"🔍 Entry: {entry.Metadata.ClrType.Name}, State: {entry.State}"); if (entry.State == EntityState.Added || entry.State == EntityState.Modified || entry.State == EntityState.Deleted) { var module = entry.Metadata.GetTableName() ?? entry.Metadata.ClrType.Name; var id = GetEntityId(entry); var operation = entry.State switch { EntityState.Added => "created", EntityState.Modified => "updated", EntityState.Deleted => "deleted", _ => "unknown" }; Console.WriteLine($"🔍 Detected change: {module} {id} {operation}"); if (id != null) { _pendingChanges.Add((module, id, operation)); Console.WriteLine($"✅ Added to pending changes: {module} {id} {operation}"); } else { Console.WriteLine($"⚠️ Skipped (id is null): {module} {operation}"); } } } Console.WriteLine($"🔍 Total pending changes: {_pendingChanges.Count}"); } return base.SavingChangesAsync(eventData, result, cancellationToken); } public override async ValueTask SavedChangesAsync( SaveChangesCompletedEventData eventData, int result, CancellationToken cancellationToken = default) { // Broadcast changes AFTER successful save if (_hubContext != null && result > 0 && _pendingChanges.Any()) { _logger?.LogInformation("📤 Broadcasting {Count} entity changes via SignalR", _pendingChanges.Count); Console.WriteLine($"📤 Broadcasting {_pendingChanges.Count} entity changes via SignalR"); foreach (var (module, id, operation) in _pendingChanges) { try { Console.WriteLine($"📤 Broadcasting: {module} {id} {operation}"); // Use reflection to call hub methods since we can't reference the API project var clientsProperty = _hubContext.GetType().GetProperty("Clients"); Console.WriteLine($" 🔍 Clients property: {clientsProperty != null}"); if (clientsProperty != null) { var clients = clientsProperty.GetValue(_hubContext); Console.WriteLine($" 🔍 Clients value: {clients != null}, Type: {clients?.GetType().Name}"); if (clients != null) { var allProperty = clients.GetType().GetProperty("All"); Console.WriteLine($" 🔍 All property: {allProperty != null}"); if (allProperty != null) { var allClients = allProperty.GetValue(clients); Console.WriteLine($" 🔍 AllClients value: {allClients != null}, Type: {allClients?.GetType().Name}"); if (allClients != null) { // SendAsync is an extension method, so we need to find it differently // Look for the IClientProxy interface which has SendCoreAsync var sendCoreAsyncMethod = allClients.GetType().GetMethod("SendCoreAsync"); Console.WriteLine($" 🔍 SendCoreAsync method found: {sendCoreAsyncMethod != null}"); if (sendCoreAsyncMethod != null) { // SendCoreAsync takes (string method, object?[] args, CancellationToken cancellationToken) var task = sendCoreAsyncMethod.Invoke(allClients, new object[] { "EntityChanged", new object[] { new { module, id, operation } }, cancellationToken }) as Task; Console.WriteLine($" 🔍 Task created: {task != null}"); if (task != null) { await task; Console.WriteLine($"✅ Broadcast successful: {module} {id} {operation}"); } else { Console.WriteLine($"❌ Task is null after invoke"); } } else { Console.WriteLine($"❌ SendCoreAsync method not found"); } } } } } } catch (Exception ex) { _logger?.LogError(ex, "❌ Failed to broadcast entity change"); Console.WriteLine($"❌ Failed to broadcast: {ex.Message}"); Console.WriteLine($"❌ Stack trace: {ex.StackTrace}"); } } } _pendingChanges.Clear(); return await base.SavedChangesAsync(eventData, result, cancellationToken); } private static string? GetEntityId(Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry entry) { var keyProperty = entry.Metadata.FindPrimaryKey()?.Properties.FirstOrDefault(); if (keyProperty == null) return null; var value = entry.Property(keyProperty.Name).CurrentValue; return value?.ToString(); } }