using CredentialManager.Data; using CredentialManager.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace CredentialManager.Services; /// /// Servizio per la gestione delle associazioni basate sui valori delle chiavi /// public class KeyAssociationService : IKeyAssociationService { private readonly CredentialDbContext _context; private readonly ILogger _logger; public KeyAssociationService( CredentialDbContext context, ILogger logger) { _context = context; _logger = logger; } public async Task SaveAssociationAsync(KeyAssociation association) { // Cattura i valori critici all'inizio per evitare race conditions var keyValue = association.KeyValue; var destinationEntity = association.DestinationEntity; var destinationId = association.DestinationId; var restCredentialName = association.RestCredentialName; var sourceKeyField = association.SourceKeyField; var destinationKeyField = association.DestinationKeyField; var additionalInfo = association.AdditionalInfo; var dataHash = association.Data_Hash; var currentTime = DateTime.UtcNow; try { _logger.LogInformation("DEBUG: Tentativo salvataggio associazione - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', DestinationId: '{DestinationId}', RestCredentialName: '{RestCredentialName}'", keyValue, destinationEntity, destinationId, restCredentialName); // Implementazione thread-safe usando upsert pattern // Prima tenta di aggiornare un record esistente var rowsAffected = await _context.Database.ExecuteSqlRawAsync(@" UPDATE KeyAssociations SET DestinationId = {0}, SourceKeyField = {1}, DestinationKeyField = {2}, UpdatedAt = {3}, LastVerifiedAt = {4}, AdditionalInfo = {5}, Data_Hash = {6} WHERE KeyValue = {7} AND DestinationEntity = {8} AND RestCredentialName = {9} AND IsActive = 1", destinationId, sourceKeyField, destinationKeyField, currentTime, currentTime, additionalInfo ?? (object)DBNull.Value, dataHash ?? (object)DBNull.Value, keyValue, destinationEntity, restCredentialName); if (rowsAffected > 0) { // Recupera l'ID dell'associazione aggiornata var existing = await _context.KeyAssociations .FirstOrDefaultAsync(ka => ka.KeyValue == keyValue && ka.DestinationEntity == destinationEntity && ka.RestCredentialName == restCredentialName && ka.IsActive); if (existing != null) { // Aggiorna le informazioni sulle sorgenti usando l'entità tracciata UpdateSourcesInfo(existing, association); await _context.SaveChangesAsync(); _logger.LogInformation("Associazione aggiornata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}", keyValue, destinationEntity, destinationId); return existing.Id; } } // Se l'aggiornamento non ha modificato nessuna riga, tenta l'inserimento try { var newAssociation = new KeyAssociation { KeyValue = keyValue, SourceKeyField = sourceKeyField, DestinationKeyField = destinationKeyField, DestinationEntity = destinationEntity, DestinationId = destinationId, RestCredentialName = restCredentialName, CreatedAt = currentTime, LastVerifiedAt = currentTime, AdditionalInfo = additionalInfo, Data_Hash = dataHash, IsActive = true }; _context.KeyAssociations.Add(newAssociation); await _context.SaveChangesAsync(); _logger.LogInformation("Nuova associazione creata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}", keyValue, destinationEntity, destinationId); return newAssociation.Id; } catch (Microsoft.EntityFrameworkCore.DbUpdateException dbEx) when (dbEx.InnerException?.Message?.Contains("UNIQUE constraint failed") == true) { // Race condition: un altro thread ha inserito la stessa associazione // Ritenta la ricerca e aggiornamento _logger.LogDebug("Race condition rilevata durante inserimento, ritento con aggiornamento per KeyValue: {KeyValue}", keyValue); var existing = await _context.KeyAssociations .FirstOrDefaultAsync(ka => ka.KeyValue == keyValue && ka.DestinationEntity == destinationEntity && ka.RestCredentialName == restCredentialName && ka.IsActive); if (existing != null) { // Aggiorna l'associazione trovata existing.DestinationId = destinationId; existing.SourceKeyField = sourceKeyField; existing.DestinationKeyField = destinationKeyField; existing.UpdatedAt = currentTime; existing.LastVerifiedAt = currentTime; existing.AdditionalInfo = additionalInfo; existing.Data_Hash = dataHash; UpdateSourcesInfo(existing, association); _context.KeyAssociations.Update(existing); await _context.SaveChangesAsync(); _logger.LogInformation("Associazione aggiornata dopo race condition: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}", keyValue, destinationEntity, destinationId); return existing.Id; } // Se non riusciamo a trovare neanche l'associazione creata da un altro thread, rilancia l'eccezione throw; } } catch (Exception ex) { _logger.LogError(ex, "Errore nel salvare l'associazione: KeyValue={KeyValue} -> {DestinationEntity}", keyValue, destinationEntity); throw; } } /// /// Versione thread-safe del SaveAssociationAsync che utilizza un DbContext separato per operazioni parallele /// public async Task SaveAssociationParallelAsync(KeyAssociation association) { // Cattura i valori critici all'inizio per evitare race conditions var keyValue = association.KeyValue; var destinationEntity = association.DestinationEntity; var destinationId = association.DestinationId; var restCredentialName = association.RestCredentialName; var sourceKeyField = association.SourceKeyField; var destinationKeyField = association.DestinationKeyField; var mappedDestinationField = association.MappedDestinationField; // AGGIUNTO var additionalInfo = association.AdditionalInfo; var dataHash = association.Data_Hash; var currentTime = DateTime.UtcNow; // Crea un nuovo DbContext per questa operazione parallela var options = new DbContextOptionsBuilder() .UseSqlite(_context.Database.GetConnectionString()) .Options; using var parallelContext = new CredentialDbContext(options); try { _logger.LogDebug("PARALLEL: Tentativo salvataggio associazione - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', DestinationId: '{DestinationId}', RestCredentialName: '{RestCredentialName}', MappedField: '{MappedField}'", keyValue, destinationEntity, destinationId, restCredentialName, mappedDestinationField ?? "NULL"); // Implementazione thread-safe usando upsert pattern con DbContext separato // Prima tenta di aggiornare un record esistente var rowsAffected = await parallelContext.Database.ExecuteSqlRawAsync(@" UPDATE KeyAssociations SET DestinationId = {0}, SourceKeyField = {1}, DestinationKeyField = {2}, UpdatedAt = {3}, LastVerifiedAt = {4}, AdditionalInfo = {5}, Data_Hash = {6}, MappedDestinationField = {7} WHERE KeyValue = {8} AND DestinationEntity = {9} AND RestCredentialName = {10} AND IsActive = 1", destinationId, sourceKeyField, destinationKeyField, currentTime, currentTime, additionalInfo ?? (object)DBNull.Value, dataHash ?? (object)DBNull.Value, mappedDestinationField ?? (object)DBNull.Value, keyValue, destinationEntity, restCredentialName); if (rowsAffected > 0) { // Recupera l'ID dell'associazione aggiornata var existing = await parallelContext.KeyAssociations .FirstOrDefaultAsync(ka => ka.KeyValue == keyValue && ka.DestinationEntity == destinationEntity && ka.RestCredentialName == restCredentialName && ka.IsActive); if (existing != null) { // Aggiorna le informazioni sulle sorgenti usando l'entità tracciata UpdateSourcesInfo(existing, association); await parallelContext.SaveChangesAsync(); _logger.LogDebug("PARALLEL: Associazione aggiornata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}", keyValue, destinationEntity, destinationId); return existing.Id; } } // Se l'aggiornamento non ha modificato nessuna riga, tenta l'inserimento try { var newAssociation = new KeyAssociation { KeyValue = keyValue, SourceKeyField = sourceKeyField, DestinationKeyField = destinationKeyField, MappedDestinationField = mappedDestinationField, // AGGIUNTO DestinationEntity = destinationEntity, DestinationId = destinationId, RestCredentialName = restCredentialName, CreatedAt = currentTime, LastVerifiedAt = currentTime, AdditionalInfo = additionalInfo, Data_Hash = dataHash, IsActive = true }; parallelContext.KeyAssociations.Add(newAssociation); await parallelContext.SaveChangesAsync(); _logger.LogDebug("PARALLEL: Nuova associazione creata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}, MappedField={MappedField}", keyValue, destinationEntity, destinationId, mappedDestinationField ?? "NULL"); return newAssociation.Id; } catch (Microsoft.EntityFrameworkCore.DbUpdateException dbEx) when (dbEx.InnerException?.Message?.Contains("UNIQUE constraint failed") == true) { // Race condition: un altro thread ha inserito la stessa associazione // Ritenta la ricerca e aggiornamento _logger.LogDebug("PARALLEL: Race condition rilevata durante inserimento, ritento con aggiornamento per KeyValue: {KeyValue}", keyValue); var existing = await parallelContext.KeyAssociations .FirstOrDefaultAsync(ka => ka.KeyValue == keyValue && ka.DestinationEntity == destinationEntity && ka.RestCredentialName == restCredentialName && ka.IsActive); if (existing != null) { // Aggiorna l'associazione trovata existing.DestinationId = destinationId; existing.SourceKeyField = sourceKeyField; existing.DestinationKeyField = destinationKeyField; existing.UpdatedAt = currentTime; existing.LastVerifiedAt = currentTime; existing.AdditionalInfo = additionalInfo; UpdateSourcesInfo(existing, association); parallelContext.KeyAssociations.Update(existing); await parallelContext.SaveChangesAsync(); _logger.LogDebug("PARALLEL: Associazione aggiornata dopo race condition: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}", keyValue, destinationEntity, destinationId); return existing.Id; } // Se non riusciamo a trovare neanche l'associazione creata da un altro thread, rilancia l'eccezione throw; } } catch (Exception ex) { _logger.LogError(ex, "PARALLEL: Errore nel salvare l'associazione: KeyValue={KeyValue} -> {DestinationEntity}", keyValue, destinationEntity); throw; } } /// /// Versione thread-safe per operazioni parallele - Find association by key value /// public async Task FindAssociationByKeyValueParallelAsync(string keyValue, string destinationEntity, string restCredentialName) { var options = new DbContextOptionsBuilder() .UseSqlite(_context.Database.GetConnectionString()) .Options; using var parallelContext = new CredentialDbContext(options); try { _logger.LogDebug("PARALLEL: Ricerca associazione con parametri - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', RestCredentialName: '{RestCredentialName}'", keyValue, destinationEntity, restCredentialName); var result = await parallelContext.KeyAssociations .FirstOrDefaultAsync(ka => ka.KeyValue == keyValue && ka.DestinationEntity == destinationEntity && ka.RestCredentialName == restCredentialName && ka.IsActive); _logger.LogDebug("PARALLEL: Risultato ricerca associazione: {Found}. ID: {Id}, DestinationId: '{DestinationId}'", result != null, result?.Id, result?.DestinationId); return result; } catch (Exception ex) { _logger.LogError(ex, "PARALLEL: Errore nella ricerca dell'associazione: KeyValue={KeyValue} -> {DestinationEntity}", keyValue, destinationEntity); throw; } } /// /// Versione thread-safe per operazioni parallele - Find association by key value only /// public async Task FindAssociationByKeyValueParallelAsync(string keyValue) { var options = new DbContextOptionsBuilder() .UseSqlite(_context.Database.GetConnectionString()) .Options; using var parallelContext = new CredentialDbContext(options); try { return await parallelContext.KeyAssociations .Where(ka => ka.KeyValue == keyValue && ka.IsActive) .OrderByDescending(ka => ka.UpdatedAt ?? ka.CreatedAt) .FirstOrDefaultAsync(); } catch (Exception ex) { _logger.LogError(ex, "PARALLEL: Errore nella ricerca dell'associazione per KeyValue={KeyValue}", keyValue); throw; } } /// /// Versione thread-safe per operazioni parallele - Delete association /// public async Task DeleteAssociationParallelAsync(int id) { var options = new DbContextOptionsBuilder() .UseSqlite(_context.Database.GetConnectionString()) .Options; using var parallelContext = new CredentialDbContext(options); try { var association = await parallelContext.KeyAssociations.FindAsync(id); if (association == null) { _logger.LogWarning("PARALLEL: Associazione con ID {Id} non trovata per l'eliminazione", id); return false; } parallelContext.KeyAssociations.Remove(association); await parallelContext.SaveChangesAsync(); _logger.LogDebug("PARALLEL: Associazione eliminata: ID {Id}", id); return true; } catch (Exception ex) { _logger.LogError(ex, "PARALLEL: Errore nell'eliminazione dell'associazione: ID {Id}", id); throw; } } /// /// Versione thread-safe per operazioni parallele - Salva una associazione con parametri semplici /// public async Task SaveAssociationParallelAsync(string keyValue, string destinationEntity, string destinationId, string restCredentialName) { var options = new DbContextOptionsBuilder() .UseSqlite(_context.Database.GetConnectionString()) .Options; using var parallelContext = new CredentialDbContext(options); try { _logger.LogDebug("PARALLEL: Salvataggio associazione - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', DestinationId: '{DestinationId}', RestCredentialName: '{RestCredentialName}'", keyValue, destinationEntity, destinationId, restCredentialName); var currentTime = DateTime.UtcNow; // Implementazione thread-safe usando upsert pattern var rowsAffected = await parallelContext.Database.ExecuteSqlRawAsync(@" UPDATE KeyAssociations SET DestinationId = {0}, UpdatedAt = {1}, LastVerifiedAt = {2}, IsActive = 1 WHERE KeyValue = {3} AND DestinationEntity = {4} AND RestCredentialName = {5}", destinationId, currentTime, currentTime, keyValue, destinationEntity, restCredentialName); if (rowsAffected == 0) { // Se nessun record è stato aggiornato, inserisci un nuovo record await parallelContext.Database.ExecuteSqlRawAsync(@" INSERT INTO KeyAssociations (KeyValue, DestinationEntity, DestinationId, RestCredentialName, CreatedAt, UpdatedAt, LastVerifiedAt, IsActive) VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, 1)", keyValue, destinationEntity, destinationId, restCredentialName, currentTime, currentTime, currentTime); _logger.LogDebug("PARALLEL: Nuova associazione creata: {KeyValue} -> {DestinationEntity}({DestinationId})", keyValue, destinationEntity, destinationId); } else { _logger.LogDebug("PARALLEL: Associazione aggiornata: {KeyValue} -> {DestinationEntity}({DestinationId})", keyValue, destinationEntity, destinationId); } return true; } catch (Exception ex) { _logger.LogError(ex, "PARALLEL: Errore nel salvare l'associazione: KeyValue={KeyValue} -> {DestinationEntity}", keyValue, destinationEntity); throw; } } public async Task FindAssociationByKeyValueAsync(string keyValue, string destinationEntity, string restCredentialName) { try { _logger.LogInformation("DEBUG: Ricerca associazione con parametri - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', RestCredentialName: '{RestCredentialName}'", keyValue, destinationEntity, restCredentialName); var result = await _context.KeyAssociations .FirstOrDefaultAsync(ka => ka.KeyValue == keyValue && ka.DestinationEntity == destinationEntity && ka.RestCredentialName == restCredentialName && ka.IsActive); _logger.LogInformation("DEBUG: Risultato ricerca associazione: {Found}. ID: {Id}, DestinationId: '{DestinationId}'", result != null, result?.Id, result?.DestinationId); return result; } catch (Exception ex) { _logger.LogError(ex, "Errore nella ricerca dell'associazione: KeyValue={KeyValue} -> {DestinationEntity}", keyValue, destinationEntity); throw; } } public async Task FindAssociationByKeyValueAsync(string keyValue) { try { return await _context.KeyAssociations .Where(ka => ka.KeyValue == keyValue && ka.IsActive) .OrderByDescending(ka => ka.UpdatedAt ?? ka.CreatedAt) .FirstOrDefaultAsync(); } catch (Exception ex) { _logger.LogError(ex, "Errore nella ricerca dell'associazione per KeyValue={KeyValue}", keyValue); throw; } } public async Task> GetAssociationsByDestinationAsync(string destinationEntity, string restCredentialName) { try { return await _context.KeyAssociations .Where(ka => ka.DestinationEntity == destinationEntity && ka.RestCredentialName == restCredentialName && ka.IsActive) .OrderByDescending(ka => ka.CreatedAt) .ToListAsync(); } catch (Exception ex) { _logger.LogError(ex, "Errore nel recupero delle associazioni per destinazione: {DestinationEntity} ({RestCredentialName})", destinationEntity, restCredentialName); throw; } } public async Task> GetAllActiveAssociationsAsync() { try { return await _context.KeyAssociations .Where(ka => ka.IsActive) .OrderByDescending(ka => ka.CreatedAt) .ToListAsync(); } catch (Exception ex) { _logger.LogError(ex, "Errore nel recupero di tutte le associazioni attive"); throw; } } public async Task> GetAllAssociationsAsync() { try { return await _context.KeyAssociations .OrderByDescending(ka => ka.CreatedAt) .ToListAsync(); } catch (Exception ex) { _logger.LogError(ex, "Errore nel recupero di tutte le associazioni"); throw; } } public async Task UpdateAssociationAsync(KeyAssociation association) { try { var existing = await _context.KeyAssociations.FindAsync(association.Id); if (existing == null) { _logger.LogWarning("Associazione con ID {Id} non trovata per l'aggiornamento", association.Id); return false; } existing.KeyValue = association.KeyValue; existing.SourceKeyField = association.SourceKeyField; existing.DestinationKeyField = association.DestinationKeyField; existing.DestinationId = association.DestinationId; existing.RestCredentialName = association.RestCredentialName; existing.UpdatedAt = DateTime.UtcNow; existing.LastVerifiedAt = association.LastVerifiedAt; existing.AdditionalInfo = association.AdditionalInfo; existing.SourcesInfo = association.SourcesInfo; existing.IsActive = association.IsActive; existing.Data_Hash = association.Data_Hash; _context.KeyAssociations.Update(existing); await _context.SaveChangesAsync(); _logger.LogInformation("Associazione aggiornata: ID {Id}", association.Id); return true; } catch (Exception ex) { _logger.LogError(ex, "Errore nell'aggiornamento dell'associazione: ID {Id}", association.Id); throw; } } public async Task DeactivateAssociationAsync(int id) { try { var association = await _context.KeyAssociations.FindAsync(id); if (association == null) { _logger.LogWarning("Associazione con ID {Id} non trovata per la disattivazione", id); return false; } association.IsActive = false; association.UpdatedAt = DateTime.UtcNow; _context.KeyAssociations.Update(association); await _context.SaveChangesAsync(); _logger.LogInformation("Associazione disattivata: ID {Id}", id); return true; } catch (Exception ex) { _logger.LogError(ex, "Errore nella disattivazione dell'associazione: ID {Id}", id); throw; } } public async Task DeleteAssociationAsync(int id) { try { var association = await _context.KeyAssociations.FindAsync(id); if (association == null) { _logger.LogWarning("Associazione con ID {Id} non trovata per l'eliminazione", id); return false; } _context.KeyAssociations.Remove(association); await _context.SaveChangesAsync(); _logger.LogInformation("Associazione eliminata: ID {Id}", id); return true; } catch (Exception ex) { _logger.LogError(ex, "Errore nell'eliminazione dell'associazione: ID {Id}", id); throw; } } public async Task CleanupOldAssociationsAsync(TimeSpan olderThan) { try { var cutoffDate = DateTime.UtcNow - olderThan; var oldAssociations = await _context.KeyAssociations .Where(ka => ka.CreatedAt < cutoffDate && !ka.IsActive) .ToListAsync(); if (oldAssociations.Any()) { _context.KeyAssociations.RemoveRange(oldAssociations); await _context.SaveChangesAsync(); _logger.LogInformation("Pulite {Count} associazioni obsolete più vecchie di {Cutoff}", oldAssociations.Count, cutoffDate); } return oldAssociations.Count; } catch (Exception ex) { _logger.LogError(ex, "Errore nella pulizia delle associazioni obsolete"); throw; } } public async Task ClearAssociationsAsync(string destinationEntity, string restCredentialName) { try { var associationsToDelete = await _context.KeyAssociations .Where(ka => ka.DestinationEntity == destinationEntity && ka.RestCredentialName == restCredentialName) .ToListAsync(); if (associationsToDelete.Any()) { _context.KeyAssociations.RemoveRange(associationsToDelete); await _context.SaveChangesAsync(); _logger.LogInformation("Eliminate {Count} associazioni per {DestinationEntity}", associationsToDelete.Count, destinationEntity); } return associationsToDelete.Count; } catch (Exception ex) { _logger.LogError(ex, "Errore nella cancellazione delle associazioni per {DestinationEntity}", destinationEntity); throw; } } public async Task ClearAllAssociationsAsync() { try { var allAssociations = await _context.KeyAssociations.ToListAsync(); var count = allAssociations.Count; if (allAssociations.Any()) { _context.KeyAssociations.RemoveRange(allAssociations); await _context.SaveChangesAsync(); _logger.LogWarning("Eliminate TUTTE le {Count} associazioni dal sistema", count); } return count; } catch (Exception ex) { _logger.LogError(ex, "Errore nella cancellazione di tutte le associazioni"); throw; } } public async Task ValidateDestinationIdAsync(string destinationId, string destinationEntity, string restCredentialName) { // Questa implementazione base restituisce sempre true // Dovrebbe essere estesa per verificare effettivamente l'esistenza nel sistema REST try { // TODO: Implementare la logica di validazione effettiva con il servizio REST // Per ora assumiamo che l'ID sia valido _logger.LogDebug("Validazione ID destinazione {DestinationId} per entità {DestinationEntity} - Non implementata", destinationId, destinationEntity); return await Task.FromResult(true); } catch (Exception ex) { _logger.LogError(ex, "Errore nella validazione dell'ID destinazione {DestinationId}", destinationId); return false; } } public async Task> GetInvalidAssociationsAsync(string destinationEntity, string restCredentialName) { try { var associations = await _context.KeyAssociations .Where(ka => ka.DestinationEntity == destinationEntity && ka.RestCredentialName == restCredentialName && ka.IsActive) .ToListAsync(); var invalidAssociations = new List(); // Verifica ogni associazione foreach (var association in associations) { var isValid = await ValidateDestinationIdAsync(association.DestinationId, destinationEntity, restCredentialName); if (!isValid) { invalidAssociations.Add(association); } } _logger.LogInformation("Trovate {Invalid}/{Total} associazioni non valide per {DestinationEntity}", invalidAssociations.Count, associations.Count, destinationEntity); return invalidAssociations; } catch (Exception ex) { _logger.LogError(ex, "Errore nel recupero delle associazioni non valide per {DestinationEntity}", destinationEntity); throw; } } public async Task CleanupInvalidAssociationsAsync(string destinationEntity, string restCredentialName) { try { var invalidAssociations = await GetInvalidAssociationsAsync(destinationEntity, restCredentialName); if (invalidAssociations.Any()) { _context.KeyAssociations.RemoveRange(invalidAssociations); await _context.SaveChangesAsync(); _logger.LogWarning("Eliminate {Count} associazioni non valide per {DestinationEntity}", invalidAssociations.Count, destinationEntity); } return invalidAssociations.Count; } catch (Exception ex) { _logger.LogError(ex, "Errore nella pulizia delle associazioni non valide per {DestinationEntity}", destinationEntity); throw; } } public async Task UpdateLastVerifiedAsync(int id) { try { var association = await _context.KeyAssociations.FindAsync(id); if (association == null) { _logger.LogWarning("Associazione con ID {Id} non trovata per l'aggiornamento della verifica", id); return false; } association.LastVerifiedAt = DateTime.UtcNow; _context.KeyAssociations.Update(association); await _context.SaveChangesAsync(); return true; } catch (Exception ex) { _logger.LogError(ex, "Errore nell'aggiornamento della verifica per associazione: ID {Id}", id); throw; } } public async Task GetStatisticsAsync() { try { var allAssociations = await _context.KeyAssociations.ToListAsync(); var stats = new AssociationStatistics { TotalAssociations = allAssociations.Count, ActiveAssociations = allAssociations.Count(a => a.IsActive), InactiveAssociations = allAssociations.Count(a => !a.IsActive), UniqueKeyValues = allAssociations.Select(a => a.KeyValue).Distinct().Count(), UniqueDestinationEntities = allAssociations.Select(a => a.DestinationEntity).Distinct().Count(), OldestAssociation = allAssociations.Any() ? allAssociations.Min(a => a.CreatedAt) : null, NewestAssociation = allAssociations.Any() ? allAssociations.Max(a => a.CreatedAt) : null, AssociationsByEntity = allAssociations .GroupBy(a => a.DestinationEntity) .ToDictionary(g => g.Key, g => g.Count()) }; return stats; } catch (Exception ex) { _logger.LogError(ex, "Errore nel calcolo delle statistiche delle associazioni"); throw; } } private void UpdateSourcesInfo(KeyAssociation existing, KeyAssociation newAssociation) { try { var sourcesInfo = existing.SourcesInfo ?? ""; var timestamp = DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm"); var newSourceInfo = $"{timestamp}: {newAssociation.SourceKeyField}"; if (!sourcesInfo.Contains(newSourceInfo)) { existing.SourcesInfo = string.IsNullOrEmpty(sourcesInfo) ? newSourceInfo : $"{sourcesInfo}; {newSourceInfo}"; } } catch (Exception ex) { _logger.LogWarning(ex, "Errore nell'aggiornamento delle informazioni sulle sorgenti"); } } }