202 lines
9.1 KiB
C#
202 lines
9.1 KiB
C#
|
|
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<EntityChangeInterceptor>? _logger;
|
||
|
|
private readonly List<(string Module, string Id, string Operation)> _pendingChanges = new();
|
||
|
|
|
||
|
|
public EntityChangeInterceptor(IServiceProvider serviceProvider)
|
||
|
|
{
|
||
|
|
_logger = serviceProvider.GetService(typeof(ILogger<EntityChangeInterceptor>)) as ILogger<EntityChangeInterceptor>;
|
||
|
|
|
||
|
|
// 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<InterceptionResult<int>> SavingChangesAsync(
|
||
|
|
DbContextEventData eventData,
|
||
|
|
InterceptionResult<int> 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<int> 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();
|
||
|
|
}
|
||
|
|
}
|