Files
Data-Coupler/CredentialManager/Services/KeyAssociationService.cs
T
Alessio 04f0403f12 feat: Implementato sistema di associazioni chiave per prevenire duplicati nel data coupling
BREAKING CHANGE: Rimosso completamente il vecchio sistema RecordAssociation

Modifiche principali:
- Sostituito RecordAssociation con KeyAssociation basato sui valori delle chiavi
- Implementata logica robusta di UPDATE vs INSERT basata su associazioni esistenti
- Aggiunta normalizzazione delle chiavi (.Trim()) per consistenza
- Implementato fallback nella ricerca associazioni per maggiore affidabilità
- Sostituita verifica pre-UPDATE con tentativo diretto più efficiente

Componenti modificati:
- Nuovo modello: KeyAssociation.cs con campi ottimizzati
- Nuovo servizio: KeyAssociationService.cs con metodi completi
- Aggiornato: DataCoupler.razor con logica migliorata di gestione associazioni
- Aggiornato: CredentialDbContext per gestire solo KeyAssociations
- Aggiornati: tutti i servizi di interfaccia per supportare il nuovo sistema
- Creata: pagina KeyAssociations.razor per gestione associazioni
- Aggiornato: NavMenu.razor con link alla gestione associazioni

Miglioramenti tecnici:
- Logica di UPDATE più robusta: tenta direttamente l'aggiornamento invece di verificare prima l'esistenza
- Gestione errori migliorata con cleanup automatico delle associazioni non valide
- Debug logging estensivo per troubleshooting
- Fallback nella ricerca associazioni se parametri specifici falliscono
- Normalizzazione valori chiave per prevenire problemi di whitespace

Risultato:
Il sistema ora previene correttamente i duplicati utilizzando le associazioni per decidere
se fare INSERT (nuovo record) o UPDATE (record esistente) basandosi sui valori delle chiavi.

Database:
- Creata migrazione EF per rimuovere RecordAssociations e aggiungere KeyAssociations
- Eliminati file e codice legacy non più necessari
2025-06-29 20:44:20 +02:00

495 lines
18 KiB
C#

using CredentialManager.Data;
using CredentialManager.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CredentialManager.Services;
/// <summary>
/// Servizio per la gestione delle associazioni basate sui valori delle chiavi
/// </summary>
public class KeyAssociationService : IKeyAssociationService
{
private readonly CredentialDbContext _context;
private readonly ILogger<KeyAssociationService> _logger;
public KeyAssociationService(
CredentialDbContext context,
ILogger<KeyAssociationService> logger)
{
_context = context;
_logger = logger;
}
public async Task<int> SaveAssociationAsync(KeyAssociation association)
{
try
{
_logger.LogInformation("DEBUG: Tentativo salvataggio associazione - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', DestinationId: '{DestinationId}', RestCredentialName: '{RestCredentialName}'",
association.KeyValue, association.DestinationEntity, association.DestinationId, association.RestCredentialName);
// Controlla se esiste già un'associazione per questo valore chiave e destinazione
var existing = await _context.KeyAssociations
.FirstOrDefaultAsync(ka =>
ka.KeyValue == association.KeyValue &&
ka.DestinationEntity == association.DestinationEntity &&
ka.RestCredentialName == association.RestCredentialName &&
ka.IsActive);
_logger.LogInformation("DEBUG: Controllo associazione esistente: {Found}. ID: {Id}",
existing != null, existing?.Id);
if (existing != null)
{
// Aggiorna l'associazione esistente
existing.DestinationId = association.DestinationId;
existing.SourceKeyField = association.SourceKeyField;
existing.DestinationKeyField = association.DestinationKeyField;
existing.UpdatedAt = DateTime.UtcNow;
existing.LastVerifiedAt = DateTime.UtcNow;
existing.AdditionalInfo = association.AdditionalInfo;
// Aggiorna le informazioni sulle sorgenti
UpdateSourcesInfo(existing, association);
_context.KeyAssociations.Update(existing);
await _context.SaveChangesAsync();
_logger.LogInformation("Associazione aggiornata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}",
association.KeyValue, association.DestinationEntity, association.DestinationId);
return existing.Id;
}
else
{
// Crea nuova associazione
association.CreatedAt = DateTime.UtcNow;
association.LastVerifiedAt = DateTime.UtcNow;
_context.KeyAssociations.Add(association);
await _context.SaveChangesAsync();
_logger.LogInformation("Nuova associazione creata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}",
association.KeyValue, association.DestinationEntity, association.DestinationId);
return association.Id;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel salvare l'associazione: KeyValue={KeyValue} -> {DestinationEntity}",
association.KeyValue, association.DestinationEntity);
throw;
}
}
public async Task<KeyAssociation?> 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<KeyAssociation?> 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<List<KeyAssociation>> 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<List<KeyAssociation>> 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<List<KeyAssociation>> 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<bool> 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.AdditionalInfo = association.AdditionalInfo;
existing.SourcesInfo = association.SourcesInfo;
existing.IsActive = association.IsActive;
_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<bool> 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<bool> 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<int> 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<int> 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<int> 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<bool> 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<List<KeyAssociation>> 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<KeyAssociation>();
// 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<int> 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<bool> 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<AssociationStatistics> 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");
}
}
}