SingalR for realtime entitychanges
All checks were successful
Build Docker Images / test (map[name:Morska plugin_project:DiunaBI.Plugins.Morska]) (push) Successful in 1m36s
Build Docker Images / test (map[name:PedrolloPL plugin_project:DiunaBI.Plugins.PedrolloPL]) (push) Successful in 1m31s
Build Docker Images / build-and-push (map[image_suffix:morska name:Morska plugin_project:DiunaBI.Plugins.Morska]) (push) Successful in 1m55s
Build Docker Images / build-and-push (map[image_suffix:pedrollopl name:PedrolloPL plugin_project:DiunaBI.Plugins.PedrolloPL]) (push) Successful in 1m53s
All checks were successful
Build Docker Images / test (map[name:Morska plugin_project:DiunaBI.Plugins.Morska]) (push) Successful in 1m36s
Build Docker Images / test (map[name:PedrolloPL plugin_project:DiunaBI.Plugins.PedrolloPL]) (push) Successful in 1m31s
Build Docker Images / build-and-push (map[image_suffix:morska name:Morska plugin_project:DiunaBI.Plugins.Morska]) (push) Successful in 1m55s
Build Docker Images / build-and-push (map[image_suffix:pedrollopl name:PedrolloPL plugin_project:DiunaBI.Plugins.PedrolloPL]) (push) Successful in 1m53s
This commit is contained in:
201
DiunaBI.Infrastructure/Interceptors/EntityChangeInterceptor.cs
Normal file
201
DiunaBI.Infrastructure/Interceptors/EntityChangeInterceptor.cs
Normal file
@@ -0,0 +1,201 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user