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
This commit is contained in:
2025-06-29 20:44:20 +02:00
parent 2238ddc4bf
commit 04f0403f12
23 changed files with 2051 additions and 1161 deletions
@@ -68,16 +68,16 @@ public class DatabaseInitializer : IDatabaseInitializer
await _context.Credentials.CountAsync();
_logger.LogInformation("Tabella Credentials verificata con successo");
// Verifica se la tabella RecordAssociations esiste, se non esiste la crea senza ricreare tutto il database
// Verifica se la tabella KeyAssociations esiste, se non esiste la crea senza ricreare tutto il database
try
{
await _context.RecordAssociations.CountAsync();
_logger.LogInformation("Tabella RecordAssociations verificata con successo");
await _context.KeyAssociations.CountAsync();
_logger.LogInformation("Tabella KeyAssociations verificata con successo");
}
catch (Exception)
{
_logger.LogInformation("Tabella RecordAssociations non trovata, creazione tramite migrazione...");
await CreateRecordAssociationsTableAsync();
_logger.LogInformation("Tabella KeyAssociations non trovata, creazione tramite migrazione...");
await CreateKeyAssociationsTableAsync();
}
}
catch (Exception)
@@ -170,60 +170,78 @@ public class DatabaseInitializer : IDatabaseInitializer
_logger.LogInformation("Colonna RestServiceType aggiunta con successo");
}
// Migrazione 2: Verifica se la tabella RecordAssociations esiste
// Migrazione 2: Elimina vecchia tabella RecordAssociations se esiste e crea KeyAssociations
try
{
// Prova a eliminare la vecchia tabella se esiste
await _context.Database.ExecuteSqlRawAsync("DROP TABLE IF EXISTS RecordAssociations");
_logger.LogInformation("Vecchia tabella RecordAssociations eliminata");
}
catch (Exception)
{
// Ignora errori se la tabella non esiste
}
// Verifica se la tabella KeyAssociations esiste
try
{
await _context.Database.ExecuteSqlRawAsync(
"SELECT COUNT(*) FROM RecordAssociations LIMIT 1");
_logger.LogInformation("Tabella RecordAssociations già presente");
"SELECT COUNT(*) FROM KeyAssociations LIMIT 1");
_logger.LogInformation("Tabella KeyAssociations già presente");
}
catch (Microsoft.Data.Sqlite.SqliteException)
{
// La tabella non esiste, la creiamo
_logger.LogInformation("Creazione tabella RecordAssociations...");
_logger.LogInformation("Creazione tabella KeyAssociations...");
// Crea la tabella
await _context.Database.ExecuteSqlRawAsync(@"
CREATE TABLE RecordAssociations (
CREATE TABLE KeyAssociations (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
SourceName TEXT NOT NULL,
SourceType TEXT NOT NULL,
SourceKey TEXT NOT NULL,
KeyValue TEXT NOT NULL,
SourceKeyField TEXT NOT NULL,
DestinationKeyField TEXT NOT NULL,
DestinationEntity TEXT NOT NULL,
DestinationId TEXT NOT NULL,
RestCredentialName TEXT NOT NULL,
CreatedAt TEXT NOT NULL DEFAULT (datetime('now')),
UpdatedAt TEXT,
LastVerifiedAt TEXT,
IsActive INTEGER NOT NULL DEFAULT 1,
SourcesInfo TEXT,
AdditionalInfo TEXT
)");
// Crea gli indici
await _context.Database.ExecuteSqlRawAsync(@"
CREATE UNIQUE INDEX IX_RecordAssociations_Unique
ON RecordAssociations (SourceName, SourceKey, DestinationEntity)");
CREATE INDEX IX_KeyAssociations_KeyValue
ON KeyAssociations (KeyValue)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_SourceType
ON RecordAssociations (SourceType)");
CREATE UNIQUE INDEX IX_KeyAssociations_Unique
ON KeyAssociations (KeyValue, DestinationEntity, RestCredentialName)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_DestinationEntity
ON RecordAssociations (DestinationEntity)");
CREATE INDEX IX_KeyAssociations_DestinationEntity
ON KeyAssociations (DestinationEntity)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_RestCredentialName
ON RecordAssociations (RestCredentialName)");
CREATE INDEX IX_KeyAssociations_RestCredentialName
ON KeyAssociations (RestCredentialName)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_IsActive
ON RecordAssociations (IsActive)");
CREATE INDEX IX_KeyAssociations_IsActive
ON KeyAssociations (IsActive)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_CreatedAt
ON RecordAssociations (CreatedAt)");
CREATE INDEX IX_KeyAssociations_CreatedAt
ON KeyAssociations (CreatedAt)");
_logger.LogInformation("Tabella RecordAssociations creata con successo");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_KeyAssociations_LastVerifiedAt
ON KeyAssociations (LastVerifiedAt)");
_logger.LogInformation("Tabella KeyAssociations creata con successo");
}
}
catch (Exception ex)
@@ -233,58 +251,67 @@ public class DatabaseInitializer : IDatabaseInitializer
}
}
private async Task CreateRecordAssociationsTableAsync()
private async Task CreateKeyAssociationsTableAsync()
{
try
{
_logger.LogInformation("Creazione tabella RecordAssociations...");
_logger.LogInformation("Creazione tabella KeyAssociations...");
// Crea la tabella
// Elimina la vecchia tabella se esiste
await _context.Database.ExecuteSqlRawAsync("DROP TABLE IF EXISTS RecordAssociations");
// Crea la nuova tabella
await _context.Database.ExecuteSqlRawAsync(@"
CREATE TABLE RecordAssociations (
CREATE TABLE KeyAssociations (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
SourceName TEXT NOT NULL,
SourceType TEXT NOT NULL,
SourceKey TEXT NOT NULL,
KeyValue TEXT NOT NULL,
SourceKeyField TEXT NOT NULL,
DestinationKeyField TEXT NOT NULL,
DestinationEntity TEXT NOT NULL,
DestinationId TEXT NOT NULL,
RestCredentialName TEXT NOT NULL,
CreatedAt TEXT NOT NULL DEFAULT (datetime('now')),
UpdatedAt TEXT,
LastVerifiedAt TEXT,
IsActive INTEGER NOT NULL DEFAULT 1,
SourcesInfo TEXT,
AdditionalInfo TEXT
)");
// Crea gli indici
await _context.Database.ExecuteSqlRawAsync(@"
CREATE UNIQUE INDEX IX_RecordAssociations_Unique
ON RecordAssociations (SourceName, SourceKey, DestinationEntity)");
CREATE INDEX IX_KeyAssociations_KeyValue
ON KeyAssociations (KeyValue)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_SourceType
ON RecordAssociations (SourceType)");
CREATE UNIQUE INDEX IX_KeyAssociations_Unique
ON KeyAssociations (KeyValue, DestinationEntity, RestCredentialName)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_DestinationEntity
ON RecordAssociations (DestinationEntity)");
CREATE INDEX IX_KeyAssociations_DestinationEntity
ON KeyAssociations (DestinationEntity)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_RestCredentialName
ON RecordAssociations (RestCredentialName)");
CREATE INDEX IX_KeyAssociations_RestCredentialName
ON KeyAssociations (RestCredentialName)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_IsActive
ON RecordAssociations (IsActive)");
CREATE INDEX IX_KeyAssociations_IsActive
ON KeyAssociations (IsActive)");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_RecordAssociations_CreatedAt
ON RecordAssociations (CreatedAt)");
CREATE INDEX IX_KeyAssociations_CreatedAt
ON KeyAssociations (CreatedAt)");
_logger.LogInformation("Tabella RecordAssociations creata con successo");
await _context.Database.ExecuteSqlRawAsync(@"
CREATE INDEX IX_KeyAssociations_LastVerifiedAt
ON KeyAssociations (LastVerifiedAt)");
_logger.LogInformation("Tabella KeyAssociations creata con successo");
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella creazione della tabella RecordAssociations");
_logger.LogError(ex, "Errore nella creazione della tabella KeyAssociations");
throw;
}
}
@@ -0,0 +1,109 @@
using CredentialManager.Models;
namespace CredentialManager.Services;
/// <summary>
/// Interfaccia per il servizio di gestione delle associazioni basate sui valori delle chiavi
/// </summary>
public interface IKeyAssociationService
{
/// <summary>
/// Salva una nuova associazione o aggiorna una esistente
/// </summary>
Task<int> SaveAssociationAsync(KeyAssociation association);
/// <summary>
/// Cerca un'associazione esistente tramite valore chiave
/// </summary>
Task<KeyAssociation?> FindAssociationByKeyValueAsync(string keyValue, string destinationEntity, string restCredentialName);
/// <summary>
/// Cerca un'associazione esistente tramite valore chiave, indipendentemente dalla destinazione
/// </summary>
Task<KeyAssociation?> FindAssociationByKeyValueAsync(string keyValue);
/// <summary>
/// Ottiene tutte le associazioni per un'entità di destinazione specifica
/// </summary>
Task<List<KeyAssociation>> GetAssociationsByDestinationAsync(string destinationEntity, string restCredentialName);
/// <summary>
/// Ottiene tutte le associazioni attive
/// </summary>
Task<List<KeyAssociation>> GetAllActiveAssociationsAsync();
/// <summary>
/// Ottiene tutte le associazioni (attive e non)
/// </summary>
Task<List<KeyAssociation>> GetAllAssociationsAsync();
/// <summary>
/// Aggiorna un'associazione esistente
/// </summary>
Task<bool> UpdateAssociationAsync(KeyAssociation association);
/// <summary>
/// Disattiva un'associazione
/// </summary>
Task<bool> DeactivateAssociationAsync(int id);
/// <summary>
/// Elimina definitivamente un'associazione
/// </summary>
Task<bool> DeleteAssociationAsync(int id);
/// <summary>
/// Pulisce le associazioni più vecchie di un determinato periodo
/// </summary>
Task<int> CleanupOldAssociationsAsync(TimeSpan olderThan);
/// <summary>
/// Elimina tutte le associazioni per una specifica combinazione entità-credenziale
/// </summary>
Task<int> ClearAssociationsAsync(string destinationEntity, string restCredentialName);
/// <summary>
/// Elimina tutte le associazioni nel sistema
/// </summary>
Task<int> ClearAllAssociationsAsync();
/// <summary>
/// Verifica se un ID di destinazione esiste ancora nel sistema target
/// </summary>
Task<bool> ValidateDestinationIdAsync(string destinationId, string destinationEntity, string restCredentialName);
/// <summary>
/// Ottiene tutte le associazioni con ID di destinazione non validi
/// </summary>
Task<List<KeyAssociation>> GetInvalidAssociationsAsync(string destinationEntity, string restCredentialName);
/// <summary>
/// Pulisce le associazioni con ID di destinazione non più validi
/// </summary>
Task<int> CleanupInvalidAssociationsAsync(string destinationEntity, string restCredentialName);
/// <summary>
/// Aggiorna la data di ultima verifica per un'associazione
/// </summary>
Task<bool> UpdateLastVerifiedAsync(int id);
/// <summary>
/// Ottiene statistiche sulle associazioni
/// </summary>
Task<AssociationStatistics> GetStatisticsAsync();
}
/// <summary>
/// Statistiche sulle associazioni
/// </summary>
public class AssociationStatistics
{
public int TotalAssociations { get; set; }
public int ActiveAssociations { get; set; }
public int InactiveAssociations { get; set; }
public int UniqueKeyValues { get; set; }
public int UniqueDestinationEntities { get; set; }
public DateTime? OldestAssociation { get; set; }
public DateTime? NewestAssociation { get; set; }
public Dictionary<string, int> AssociationsByEntity { get; set; } = new();
}
@@ -1,79 +0,0 @@
using CredentialManager.Models;
namespace CredentialManager.Services;
/// <summary>
/// Interfaccia per il servizio di gestione delle associazioni record
/// </summary>
public interface IRecordAssociationService
{
/// <summary>
/// Salva una nuova associazione tra record sorgente e destinazione
/// </summary>
Task<int> SaveAssociationAsync(RecordAssociation association);
/// <summary>
/// Cerca un'associazione esistente tramite chiave sorgente
/// </summary>
Task<RecordAssociation?> FindAssociationAsync(string sourceName, string sourceKey, string destinationEntity);
/// <summary>
/// Ottiene tutte le associazioni per una sorgente specifica
/// </summary>
Task<List<RecordAssociation>> GetAssociationsBySourceAsync(string sourceName, string sourceType);
/// <summary>
/// Ottiene tutte le associazioni per un'entità di destinazione specifica
/// </summary>
Task<List<RecordAssociation>> GetAssociationsByDestinationAsync(string destinationEntity, string restCredentialName);
/// <summary>
/// Ottiene tutte le associazioni attive
/// </summary>
Task<List<RecordAssociation>> GetAllActiveAssociationsAsync();
/// <summary>
/// Aggiorna un'associazione esistente
/// </summary>
Task<bool> UpdateAssociationAsync(RecordAssociation association);
/// <summary>
/// Disattiva un'associazione (soft delete)
/// </summary>
Task<bool> DeactivateAssociationAsync(int id);
/// <summary>
/// Elimina definitivamente un'associazione
/// </summary>
Task<bool> DeleteAssociationAsync(int id);
/// <summary>
/// Pulisce le associazioni obsolete (opzionale)
/// </summary>
Task<int> CleanupOldAssociationsAsync(TimeSpan olderThan);
/// <summary>
/// Elimina tutte le associazioni per una specifica combinazione sorgente-destinazione
/// </summary>
Task<int> ClearAssociationsAsync(string sourceName, string destinationEntity, string restCredentialName);
/// <summary>
/// Elimina tutte le associazioni nel sistema
/// </summary>
Task<int> ClearAllAssociationsAsync();
/// <summary>
/// Verifica se un ID di destinazione esiste ancora nel sistema target
/// </summary>
Task<bool> ValidateDestinationIdAsync(string destinationId, string destinationEntity, string restCredentialName);
/// <summary>
/// Ottiene tutte le associazioni con ID di destinazione non validi
/// </summary>
Task<List<RecordAssociation>> GetInvalidAssociationsAsync(string destinationEntity, string restCredentialName);
/// <summary>
/// Pulisce le associazioni con ID di destinazione non più validi
/// </summary>
Task<int> CleanupInvalidAssociationsAsync(string destinationEntity, string restCredentialName);
}
@@ -0,0 +1,494 @@
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");
}
}
}
@@ -1,381 +0,0 @@
using CredentialManager.Data;
using CredentialManager.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace CredentialManager.Services;
/// <summary>
/// Servizio per la gestione delle associazioni tra record sorgente e destinazione
/// </summary>
public class RecordAssociationService : IRecordAssociationService
{
private readonly CredentialDbContext _context;
private readonly ILogger<RecordAssociationService> _logger;
public RecordAssociationService(
CredentialDbContext context,
ILogger<RecordAssociationService> logger)
{
_context = context;
_logger = logger;
}
public async Task<int> SaveAssociationAsync(RecordAssociation association)
{
try
{
// Controlla se esiste già un'associazione per questa combinazione
var existing = await _context.RecordAssociations
.FirstOrDefaultAsync(ra =>
ra.SourceName == association.SourceName &&
ra.SourceKey == association.SourceKey &&
ra.DestinationEntity == association.DestinationEntity &&
ra.IsActive);
if (existing != null)
{
// Aggiorna l'associazione esistente
existing.DestinationId = association.DestinationId;
existing.RestCredentialName = association.RestCredentialName;
existing.UpdatedAt = DateTime.UtcNow;
existing.AdditionalInfo = association.AdditionalInfo;
_context.RecordAssociations.Update(existing);
await _context.SaveChangesAsync();
_logger.LogInformation("Associazione aggiornata: {SourceName}/{SourceKey} -> {DestinationEntity}/{DestinationId}",
association.SourceName, association.SourceKey, association.DestinationEntity, association.DestinationId);
return existing.Id;
}
else
{
// Crea nuova associazione
_context.RecordAssociations.Add(association);
await _context.SaveChangesAsync();
_logger.LogInformation("Nuova associazione creata: {SourceName}/{SourceKey} -> {DestinationEntity}/{DestinationId}",
association.SourceName, association.SourceKey, association.DestinationEntity, association.DestinationId);
return association.Id;
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel salvare l'associazione: {SourceName}/{SourceKey} -> {DestinationEntity}",
association.SourceName, association.SourceKey, association.DestinationEntity);
throw;
}
}
public async Task<RecordAssociation?> FindAssociationAsync(string sourceName, string sourceKey, string destinationEntity)
{
try
{
return await _context.RecordAssociations
.FirstOrDefaultAsync(ra =>
ra.SourceName == sourceName &&
ra.SourceKey == sourceKey &&
ra.DestinationEntity == destinationEntity &&
ra.IsActive);
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella ricerca dell'associazione: {SourceName}/{SourceKey} -> {DestinationEntity}",
sourceName, sourceKey, destinationEntity);
throw;
}
}
public async Task<List<RecordAssociation>> GetAssociationsBySourceAsync(string sourceName, string sourceType)
{
try
{
return await _context.RecordAssociations
.Where(ra => ra.SourceName == sourceName && ra.SourceType == sourceType && ra.IsActive)
.OrderByDescending(ra => ra.CreatedAt)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero delle associazioni per sorgente: {SourceName} ({SourceType})",
sourceName, sourceType);
throw;
}
}
public async Task<List<RecordAssociation>> GetAssociationsByDestinationAsync(string destinationEntity, string restCredentialName)
{
try
{
return await _context.RecordAssociations
.Where(ra => ra.DestinationEntity == destinationEntity &&
ra.RestCredentialName == restCredentialName &&
ra.IsActive)
.OrderByDescending(ra => ra.CreatedAt)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero delle associazioni per destinazione: {DestinationEntity} ({RestCredentialName})",
destinationEntity, restCredentialName);
throw;
}
}
public async Task<List<RecordAssociation>> GetAllActiveAssociationsAsync()
{
try
{
return await _context.RecordAssociations
.Where(ra => ra.IsActive)
.OrderByDescending(ra => ra.CreatedAt)
.ToListAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel recupero di tutte le associazioni attive");
throw;
}
}
public async Task<bool> UpdateAssociationAsync(RecordAssociation association)
{
try
{
var existing = await _context.RecordAssociations.FindAsync(association.Id);
if (existing == null)
{
_logger.LogWarning("Associazione con ID {Id} non trovata per l'aggiornamento", association.Id);
return false;
}
existing.DestinationId = association.DestinationId;
existing.RestCredentialName = association.RestCredentialName;
existing.UpdatedAt = DateTime.UtcNow;
existing.AdditionalInfo = association.AdditionalInfo;
existing.IsActive = association.IsActive;
_context.RecordAssociations.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.RecordAssociations.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.RecordAssociations.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.RecordAssociations.FindAsync(id);
if (association == null)
{
_logger.LogWarning("Associazione con ID {Id} non trovata per l'eliminazione", id);
return false;
}
_context.RecordAssociations.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.RecordAssociations
.Where(ra => ra.CreatedAt < cutoffDate && !ra.IsActive)
.ToListAsync();
if (oldAssociations.Any())
{
_context.RecordAssociations.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 sourceName, string destinationEntity, string restCredentialName)
{
try
{
var associationsToDelete = await _context.RecordAssociations
.Where(ra => ra.SourceName == sourceName &&
ra.DestinationEntity == destinationEntity &&
ra.RestCredentialName == restCredentialName)
.ToListAsync();
if (associationsToDelete.Any())
{
_context.RecordAssociations.RemoveRange(associationsToDelete);
await _context.SaveChangesAsync();
_logger.LogInformation("Eliminate {Count} associazioni per {SourceName} -> {DestinationEntity}",
associationsToDelete.Count, sourceName, destinationEntity);
}
return associationsToDelete.Count;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella cancellazione delle associazioni per {SourceName} -> {DestinationEntity}",
sourceName, destinationEntity);
throw;
}
}
public async Task<int> ClearAllAssociationsAsync()
{
try
{
var allAssociations = await _context.RecordAssociations.ToListAsync();
var count = allAssociations.Count;
if (allAssociations.Any())
{
_context.RecordAssociations.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<RecordAssociation>> GetInvalidAssociationsAsync(string destinationEntity, string restCredentialName)
{
try
{
var associations = await _context.RecordAssociations
.Where(ra => ra.DestinationEntity == destinationEntity &&
ra.RestCredentialName == restCredentialName &&
ra.IsActive)
.ToListAsync();
var invalidAssociations = new List<RecordAssociation>();
// 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.RecordAssociations.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;
}
}
}