From 20a514068ab01c0ed229a6f198c9b50bd040be33 Mon Sep 17 00:00:00 2001 From: Alessio Dal Santo Date: Mon, 21 Jul 2025 10:59:50 +0200 Subject: [PATCH] feat: implementa campo Data_Hash per ottimizzazione trasferimenti - Aggiunge colonna "Hash Dati" nella tabella delle associazioni con visualizzazione troncata - Implementa generazione hash SHA256 che include signature dei mapping per rilevare modifiche configurazione - Modifica logica trasferimento per saltare record con hash identico (ottimizzazione prestazioni) - Corregge UpdateAssociationAsync per persistere correttamente Data_Hash e LastVerifiedAt nel database - Aggiorna hash solo in caso di trasferimento riuscito, mantenendo coerenza tra Salesforce e database locale - Migliora logging per debug del sistema di hash e associazioni Risolve il problema dei trasferimenti continui quando i mapping cambiano e ottimizza le prestazioni saltando record non modificati. --- ...0250721072200_AddDataHashField.Designer.cs | 345 ++++++++++++++++++ .../20250721072200_AddDataHashField.cs | 29 ++ .../CredentialDbContextModelSnapshot.cs | 4 + CredentialManager/Models/KeyAssociation.cs | 7 + .../Services/KeyAssociationService.cs | 29 +- CredentialManager/design_time_temp.db | Bin 0 -> 118784 bytes Data_Coupler/Pages/DataCoupler.razor.cs | 200 +++++++--- Data_Coupler/Pages/KeyAssociations.razor | 15 + 8 files changed, 578 insertions(+), 51 deletions(-) create mode 100644 CredentialManager/Migrations/20250721072200_AddDataHashField.Designer.cs create mode 100644 CredentialManager/Migrations/20250721072200_AddDataHashField.cs create mode 100644 CredentialManager/design_time_temp.db diff --git a/CredentialManager/Migrations/20250721072200_AddDataHashField.Designer.cs b/CredentialManager/Migrations/20250721072200_AddDataHashField.Designer.cs new file mode 100644 index 0000000..85c3e63 --- /dev/null +++ b/CredentialManager/Migrations/20250721072200_AddDataHashField.Designer.cs @@ -0,0 +1,345 @@ +// +using System; +using CredentialManager.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CredentialManager.Migrations +{ + [DbContext(typeof(CredentialDbContext))] + [Migration("20250721072200_AddDataHashField")] + partial class AddDataHashField + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.0"); + + modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalParameters") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CommandTimeout") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(30); + + b.Property("ConnectionString") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DatabaseName") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("DatabaseType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("EncryptedApiKey") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EncryptedAuthToken") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EncryptedPassword") + .HasColumnType("TEXT"); + + b.Property("Headers") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Host") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IgnoreSslErrors") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.Property("RestServiceType") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("TimeoutSeconds") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(100); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("Username") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DatabaseType"); + + b.HasIndex("IsActive"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("Type"); + + b.ToTable("Credentials", (string)null); + }); + + modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationCredentialId") + .HasColumnType("INTEGER"); + + b.Property("DestinationEndpoint") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("DestinationSchema") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationTable") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("FieldMappingJson") + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("LastUsedAt") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SourceCredentialId") + .HasColumnType("INTEGER"); + + b.Property("SourceCustomQuery") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("SourceDatabaseName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceFilePath") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("SourceKeyField") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceSchema") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceTable") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourceType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UseRecordAssociations") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DestinationCredentialId"); + + b.HasIndex("DestinationType"); + + b.HasIndex("IsActive"); + + b.HasIndex("LastUsedAt"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("SourceCredentialId"); + + b.HasIndex("SourceType"); + + b.ToTable("DataCouplerProfiles", (string)null); + }); + + modelBuilder.Entity("CredentialManager.Models.KeyAssociation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AdditionalInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Data_Hash") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("DestinationEntity") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DestinationKeyField") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("IsActive") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("KeyValue") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("LastVerifiedAt") + .HasColumnType("TEXT"); + + b.Property("RestCredentialName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SourceKeyField") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SourcesInfo") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("DestinationEntity"); + + b.HasIndex("IsActive"); + + b.HasIndex("KeyValue") + .HasDatabaseName("IX_KeyAssociations_KeyValue"); + + b.HasIndex("LastVerifiedAt"); + + b.HasIndex("RestCredentialName"); + + b.HasIndex("KeyValue", "DestinationEntity", "RestCredentialName") + .IsUnique() + .HasDatabaseName("IX_KeyAssociations_Unique"); + + b.ToTable("KeyAssociations", (string)null); + }); + + modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b => + { + b.HasOne("CredentialManager.Models.CredentialEntity", "DestinationCredential") + .WithMany() + .HasForeignKey("DestinationCredentialId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("CredentialManager.Models.CredentialEntity", "SourceCredential") + .WithMany() + .HasForeignKey("SourceCredentialId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("DestinationCredential"); + + b.Navigation("SourceCredential"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CredentialManager/Migrations/20250721072200_AddDataHashField.cs b/CredentialManager/Migrations/20250721072200_AddDataHashField.cs new file mode 100644 index 0000000..042dec8 --- /dev/null +++ b/CredentialManager/Migrations/20250721072200_AddDataHashField.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace CredentialManager.Migrations +{ + /// + public partial class AddDataHashField : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Data_Hash", + table: "KeyAssociations", + type: "TEXT", + maxLength: 64, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Data_Hash", + table: "KeyAssociations"); + } + } +} diff --git a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs index 4f0b450..cf770a2 100644 --- a/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs +++ b/CredentialManager/Migrations/CredentialDbContextModelSnapshot.cs @@ -249,6 +249,10 @@ namespace CredentialManager.Migrations b.Property("CreatedAt") .HasColumnType("TEXT"); + b.Property("Data_Hash") + .HasMaxLength(64) + .HasColumnType("TEXT"); + b.Property("DestinationEntity") .IsRequired() .HasMaxLength(200) diff --git a/CredentialManager/Models/KeyAssociation.cs b/CredentialManager/Models/KeyAssociation.cs index f3591f8..a3db5d4 100644 --- a/CredentialManager/Models/KeyAssociation.cs +++ b/CredentialManager/Models/KeyAssociation.cs @@ -88,4 +88,11 @@ public class KeyAssociation /// [MaxLength(2000)] public string? AdditionalInfo { get; set; } + + /// + /// Hash SHA256 dei dati dei campi sorgente mappati. + /// Utilizzato per rilevare cambiamenti nei dati sorgente e ottimizzare il trasferimento + /// + [MaxLength(64)] + public string? Data_Hash { get; set; } } diff --git a/CredentialManager/Services/KeyAssociationService.cs b/CredentialManager/Services/KeyAssociationService.cs index ab03a96..e8404e3 100644 --- a/CredentialManager/Services/KeyAssociationService.cs +++ b/CredentialManager/Services/KeyAssociationService.cs @@ -31,6 +31,7 @@ public class KeyAssociationService : IKeyAssociationService var sourceKeyField = association.SourceKeyField; var destinationKeyField = association.DestinationKeyField; var additionalInfo = association.AdditionalInfo; + var dataHash = association.Data_Hash; var currentTime = DateTime.UtcNow; try @@ -47,12 +48,13 @@ public class KeyAssociationService : IKeyAssociationService DestinationKeyField = {2}, UpdatedAt = {3}, LastVerifiedAt = {4}, - AdditionalInfo = {5} - WHERE KeyValue = {6} - AND DestinationEntity = {7} - AND RestCredentialName = {8} + 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, + destinationId, sourceKeyField, destinationKeyField, currentTime, currentTime, additionalInfo ?? (object)DBNull.Value, dataHash ?? (object)DBNull.Value, keyValue, destinationEntity, restCredentialName); if (rowsAffected > 0) @@ -92,6 +94,7 @@ public class KeyAssociationService : IKeyAssociationService CreatedAt = currentTime, LastVerifiedAt = currentTime, AdditionalInfo = additionalInfo, + Data_Hash = dataHash, IsActive = true }; @@ -125,6 +128,7 @@ public class KeyAssociationService : IKeyAssociationService existing.UpdatedAt = currentTime; existing.LastVerifiedAt = currentTime; existing.AdditionalInfo = additionalInfo; + existing.Data_Hash = dataHash; UpdateSourcesInfo(existing, association); @@ -162,6 +166,7 @@ public class KeyAssociationService : IKeyAssociationService var sourceKeyField = association.SourceKeyField; var destinationKeyField = association.DestinationKeyField; var additionalInfo = association.AdditionalInfo; + var dataHash = association.Data_Hash; var currentTime = DateTime.UtcNow; // Crea un nuovo DbContext per questa operazione parallela @@ -185,12 +190,13 @@ public class KeyAssociationService : IKeyAssociationService DestinationKeyField = {2}, UpdatedAt = {3}, LastVerifiedAt = {4}, - AdditionalInfo = {5} - WHERE KeyValue = {6} - AND DestinationEntity = {7} - AND RestCredentialName = {8} + 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, + destinationId, sourceKeyField, destinationKeyField, currentTime, currentTime, additionalInfo ?? (object)DBNull.Value, dataHash ?? (object)DBNull.Value, keyValue, destinationEntity, restCredentialName); if (rowsAffected > 0) @@ -230,6 +236,7 @@ public class KeyAssociationService : IKeyAssociationService CreatedAt = currentTime, LastVerifiedAt = currentTime, AdditionalInfo = additionalInfo, + Data_Hash = dataHash, IsActive = true }; @@ -549,9 +556,11 @@ public class KeyAssociationService : IKeyAssociationService 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(); diff --git a/CredentialManager/design_time_temp.db b/CredentialManager/design_time_temp.db new file mode 100644 index 0000000000000000000000000000000000000000..26d93bda576cddaeec8ab5f52e6e562c33a7945d GIT binary patch literal 118784 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 VAOHd&00JNY0w4eaAaFba{{h9nR#yN3 literal 0 HcmV?d00001 diff --git a/Data_Coupler/Pages/DataCoupler.razor.cs b/Data_Coupler/Pages/DataCoupler.razor.cs index 2215a4e..f555451 100644 --- a/Data_Coupler/Pages/DataCoupler.razor.cs +++ b/Data_Coupler/Pages/DataCoupler.razor.cs @@ -1727,6 +1727,7 @@ public partial class DataCoupler : ComponentBase "success" => "", "updated" => "table-info", "duplicate" => "table-warning", + "skipped" => "table-secondary", "error" => "table-danger", _ => "" }; @@ -1739,6 +1740,7 @@ public partial class DataCoupler : ComponentBase "success" => "bg-success", "updated" => "bg-info", "duplicate" => "bg-warning text-dark", + "skipped" => "bg-secondary", "error" => "bg-danger", _ => "bg-secondary" }; @@ -1751,6 +1753,7 @@ public partial class DataCoupler : ComponentBase "success" => "fa-check-circle", "updated" => "fa-edit", "duplicate" => "fa-exclamation-triangle", + "skipped" => "fa-forward", "error" => "fa-times-circle", _ => "fa-question-circle" }; @@ -1763,6 +1766,7 @@ public partial class DataCoupler : ComponentBase "success" => "Inserito", "updated" => "Aggiornato", "duplicate" => "Duplicato", + "skipped" => "Saltato", "error" => "Errore", _ => "Sconosciuto" }; @@ -1802,6 +1806,61 @@ public partial class DataCoupler : ComponentBase } } + /// + /// Genera un hash SHA256 dei dati dei campi sorgente mappati. + /// Utilizzato per rilevare cambiamenti nei dati e ottimizzare il trasferimento. + /// Include anche una signature dei campi mappati per rilevare cambi di configurazione. + /// + private string GenerateDataHash(Dictionary record) + { + try + { + // Raccoglie i valori dei campi mappati in ordine alfabetico per garantire consistenza + var mappedFields = fieldMappings.Keys.OrderBy(k => k).ToList(); + var valuesForHash = new List(); + + // PRIMO: Aggiungi la signature dei mapping per rilevare cambi di configurazione + var mappingSignature = string.Join(",", fieldMappings.OrderBy(m => m.Key).Select(m => $"{m.Key}->{m.Value}")); + valuesForHash.Add($"MAPPING_SIGNATURE={mappingSignature}"); + + // SECONDO: Aggiungi i valori dei dati per ogni campo mappato + foreach (var sourceField in mappedFields) + { + if (record.ContainsKey(sourceField)) + { + var value = record[sourceField]; + var normalizedValue = value?.ToString()?.Trim() ?? ""; + valuesForHash.Add($"{sourceField}={normalizedValue}"); + } + else + { + // Se il campo non è presente nel record, aggiungi una stringa vuota + valuesForHash.Add($"{sourceField}="); + } + } + + // Combina tutti i valori in una stringa unica + var combinedData = string.Join("|", valuesForHash); + + Logger.LogDebug("Hash dei dati generato da: {CombinedData}", combinedData); + + // Calcola l'hash SHA256 + using (var sha256 = System.Security.Cryptography.SHA256.Create()) + { + var hashBytes = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(combinedData)); + var hashString = Convert.ToHexString(hashBytes); + + Logger.LogDebug("Hash SHA256 generato: {Hash} (include signature mapping)", hashString); + return hashString; + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Errore nella generazione dell'hash dei dati"); + throw; + } + } + /// /// Gestisce la connessione al database con schema specifico /// @@ -2313,19 +2372,6 @@ public partial class DataCoupler : ComponentBase } } - - - - - - - - - - - - - private async Task GenerateUniqueProfileName(string baseName) { var uniqueName = baseName; @@ -2499,7 +2545,8 @@ public partial class DataCoupler : ComponentBase // 2. Trasforma i record e analizza le associazioni IN PARALLELO var recordsForCreate = new ConcurrentBag<(Dictionary transformedData, Dictionary originalRecord, int recordNumber)>(); - var recordsForUpdate = new ConcurrentBag<(Dictionary transformedData, string entityId, Dictionary originalRecord, int recordNumber)>(); + var recordsForUpdate = new ConcurrentBag<(Dictionary transformedData, string entityId, Dictionary originalRecord, int recordNumber, string newDataHash)>(); + var recordsSkipped = new ConcurrentBag<(Dictionary originalRecord, int recordNumber, string reason)>(); var recordErrors = new ConcurrentBag(); // Cattura i valori condivisi per evitare race conditions @@ -2524,10 +2571,11 @@ public partial class DataCoupler : ComponentBase // Trasforma il record in base ai mapping (operazione locale, thread-safe) var restData = TransformRecordToRestEntity(record); - // Genera la chiave sorgente per questo record (operazione locale, thread-safe) + // Genera la chiave sorgente e l'hash dei dati per questo record (operazioni locali, thread-safe) var sourceKey = GenerateSourceKey(record); + var currentDataHash = GenerateDataHash(record); - // Analizza le associazioni per capire se aggiornare o creare + // Analizza le associazioni per capire se aggiornare, creare o saltare if (currentUseRecordAssociations && !string.IsNullOrEmpty(sourceKey)) { Logger.LogDebug("COMPOSITE PARALLEL: Cerco associazione per KeyValue: '{KeyValue}', Entity: '{Entity}', Credential: '{Credential}'", @@ -2554,14 +2602,27 @@ public partial class DataCoupler : ComponentBase if (existingAssociation != null && existingAssociation.IsActive) { - // Record da aggiornare - recordsForUpdate.Add((restData, existingAssociation.DestinationId, record, recordNumber)); - Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} marcato per aggiornamento (EntityId: {EntityId})", - recordNumber, existingAssociation.DestinationId); + // CONTROLLO HASH: Verifica se i dati sono cambiati + var existingHash = existingAssociation.Data_Hash; + + if (!string.IsNullOrEmpty(existingHash) && existingHash.Equals(currentDataHash, StringComparison.OrdinalIgnoreCase)) + { + // I dati non sono cambiati, salta questo record + recordsSkipped.Add((record, recordNumber, "Dati non modificati (hash identico)")); + Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} saltato - hash identico: {Hash}", + recordNumber, currentDataHash); + } + else + { + // I dati sono cambiati o l'hash è vuoto, procedi con l'aggiornamento + recordsForUpdate.Add((restData, existingAssociation.DestinationId, record, recordNumber, currentDataHash)); + Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} marcato per aggiornamento (EntityId: {EntityId}) - hash diverso: old={OldHash}, new={NewHash}", + recordNumber, existingAssociation.DestinationId, existingHash ?? "NULL", currentDataHash); + } } else { - // Record da creare + // Record da creare (nessuna associazione esistente) recordsForCreate.Add((restData, record, recordNumber)); Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} marcato per creazione", recordNumber); } @@ -2601,9 +2662,22 @@ public partial class DataCoupler : ComponentBase // Converti i ConcurrentBag in liste per il resto del processing var finalRecordsForCreate = recordsForCreate.ToList(); var finalRecordsForUpdate = recordsForUpdate.ToList(); + var finalRecordsSkipped = recordsSkipped.ToList(); - Logger.LogInformation("COMPOSITE: Analisi parallela completata in {ElapsedMs}ms - {CreateCount} record da creare, {UpdateCount} record da aggiornare, {ErrorCount} errori", - analysisElapsed, finalRecordsForCreate.Count, finalRecordsForUpdate.Count, recordErrors.Count); + Logger.LogInformation("COMPOSITE: Analisi parallela completata in {ElapsedMs}ms - {CreateCount} record da creare, {UpdateCount} record da aggiornare, {SkippedCount} record saltati, {ErrorCount} errori", + analysisElapsed, finalRecordsForCreate.Count, finalRecordsForUpdate.Count, finalRecordsSkipped.Count, recordErrors.Count); + + // Aggiungi i record saltati ai risultati di trasferimento + foreach (var skipped in finalRecordsSkipped) + { + transferResults.Add(new TransferResult + { + RecordNumber = skipped.recordNumber, + RecordData = skipped.originalRecord, + Status = "skipped", + Message = skipped.reason + }); + } // 3. Esegui le chiamate composite in parallelo var createTask = Task.FromResult(new List()); @@ -2659,7 +2733,9 @@ public partial class DataCoupler : ComponentBase if (useRecordAssociations && !string.IsNullOrEmpty(transferResult.EntityId)) { // IMPORTANTE: Non awaita qui, solo crea il task per esecuzione parallela - var associationTask = CreateAssociationAsync(originalData.originalRecord, transferResult.EntityId, originalData.recordNumber); + // Genera l'hash per questo record per salvarlo nell'associazione + var dataHashForAssociation = GenerateDataHash(originalData.originalRecord); + var associationTask = CreateAssociationAsync(originalData.originalRecord, transferResult.EntityId, originalData.recordNumber, dataHashForAssociation); createAssociationTasks.Add(associationTask); } } @@ -2699,8 +2775,8 @@ public partial class DataCoupler : ComponentBase if (useRecordAssociations && !string.IsNullOrEmpty(result.EntityId)) { // IMPORTANTE: Non awaita qui, solo crea il task per esecuzione parallela - var verificationTask = UpdateAssociationVerificationAsync(result.EntityId); - updateAssociationTasks.Add(verificationTask); + var updateHashTask = UpdateAssociationHashAsync(originalData.originalRecord, result.EntityId, originalData.newDataHash); + updateAssociationTasks.Add(updateHashTask); } } else @@ -2709,13 +2785,9 @@ public partial class DataCoupler : ComponentBase transferResult.Status = "error"; transferResult.Message = $"Errore aggiornamento (Composite): {result.ErrorMessage}"; - // Aggiungi task di gestione fallimento alla lista (esecuzione parallela) - if (useRecordAssociations) - { - // IMPORTANTE: Non awaita qui, solo crea il task per esecuzione parallela - var failureTask = HandleFailedUpdateAsync(originalData.originalRecord, originalData.recordNumber); - updateAssociationTasks.Add(failureTask); - } + // NON aggiornare l'hash in caso di errore nel trasferimento + Logger.LogWarning("COMPOSITE: Trasferimento fallito per record {RecordNumber} - EntityId: {EntityId}. Hash non aggiornato.", + originalData.recordNumber, result.EntityId ?? "N/A"); } transferResults.Add(transferResult); @@ -2740,11 +2812,12 @@ public partial class DataCoupler : ComponentBase Logger.LogInformation("COMPOSITE: Nessuna operazione di associazione da eseguire"); } - // 7. Mostra risultati - ShowTransferResults(successCount, updatedCount, 0, errorCount); + // 7. Mostra risultati (inclusi i record saltati) + var skippedCount = finalRecordsSkipped.Count; + ShowTransferResults(successCount, updatedCount, 0, errorCount, skippedCount); - Logger.LogInformation("Trasferimento COMPOSITE completato. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Errori: {ErrorCount}", - successCount, updatedCount, errorCount); + Logger.LogInformation("Trasferimento COMPOSITE completato. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Saltati: {SkippedCount}, Errori: {ErrorCount}", + successCount, updatedCount, skippedCount, errorCount); } catch (Exception ex) { @@ -2758,7 +2831,7 @@ public partial class DataCoupler : ComponentBase } } - private async Task CreateAssociationAsync(Dictionary originalRecord, string entityId, int recordNumber) + private async Task CreateAssociationAsync(Dictionary originalRecord, string entityId, int recordNumber, string? dataHash = null) { try { @@ -2772,6 +2845,9 @@ public partial class DataCoupler : ComponentBase var sourceKey = GenerateSourceKey(originalRecord); if (string.IsNullOrEmpty(sourceKey)) return; + // Usa l'hash passato come parametro o genera uno nuovo se non fornito + var finalDataHash = dataHash ?? GenerateDataHash(originalRecord); + var destinationKeyField = GetEntityIdField(); var association = new KeyAssociation { @@ -2783,18 +2859,21 @@ public partial class DataCoupler : ComponentBase RestCredentialName = currentCredentialName, CreatedAt = DateTime.UtcNow, LastVerifiedAt = DateTime.UtcNow, + Data_Hash = finalDataHash, AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new { TransferDate = DateTime.UtcNow, RecordNumber = recordNumber, MappingCount = currentMappingCount, SourceType = currentSourceType, - CompositeTransfer = true + CompositeTransfer = true, + DataHashGenerated = true }) }; var associationId = await CredentialService.SaveKeyAssociationParallelAsync(association); - Logger.LogDebug("COMPOSITE: Associazione creata con ID: {AssociationId} per record {RecordNumber} (PARALLEL)", associationId, recordNumber); + Logger.LogDebug("COMPOSITE: Associazione creata con ID: {AssociationId} per record {RecordNumber} (PARALLEL) - Hash: {Hash}", + associationId, recordNumber, finalDataHash); } catch (Exception ex) { @@ -2802,6 +2881,43 @@ public partial class DataCoupler : ComponentBase } } + private async Task UpdateAssociationHashAsync(Dictionary originalRecord, string entityId, string newDataHash) + { + try + { + // Cattura i valori condivisi per evitare race conditions + var currentEntityName = selectedRestEntity?.Name ?? ""; + var currentCredentialName = selectedRestCredential ?? ""; + + var sourceKey = GenerateSourceKey(originalRecord); + if (string.IsNullOrEmpty(sourceKey)) return; + + // Trova l'associazione esistente e aggiorna l'hash + var existingAssociation = await CredentialService.FindKeyAssociationByValueParallelAsync( + sourceKey, currentEntityName, currentCredentialName); + + if (existingAssociation != null) + { + existingAssociation.Data_Hash = newDataHash; + existingAssociation.LastVerifiedAt = DateTime.UtcNow; + existingAssociation.UpdatedAt = DateTime.UtcNow; + + await CredentialService.UpdateKeyAssociationAsync(existingAssociation); + Logger.LogDebug("COMPOSITE: Hash associazione aggiornato per entityId {EntityId} - Nuovo hash: {Hash}", + entityId, newDataHash); + } + else + { + Logger.LogWarning("COMPOSITE: Associazione non trovata per aggiornamento hash - EntityId: {EntityId}, SourceKey: {SourceKey}", + entityId, sourceKey); + } + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Errore nell'aggiornamento dell'hash dell'associazione per entityId {EntityId}", entityId); + } + } + private Task UpdateAssociationVerificationAsync(string entityId) { try @@ -2840,7 +2956,7 @@ public partial class DataCoupler : ComponentBase } } - private void ShowTransferResults(int successCount, int updatedCount, int duplicateCount, int errorCount) + private void ShowTransferResults(int successCount, int updatedCount, int duplicateCount, int errorCount, int skippedCount = 0) { if (errorCount == 0) { @@ -2849,6 +2965,7 @@ public partial class DataCoupler : ComponentBase if (successCount > 0) messageParts.Add($"{successCount} record inseriti"); if (updatedCount > 0) messageParts.Add($"{updatedCount} record aggiornati"); + if (skippedCount > 0) messageParts.Add($"{skippedCount} record saltati (dati non modificati)"); if (duplicateCount > 0) messageParts.Add($"{duplicateCount} duplicati rilevati (warning)"); message += string.Join(", ", messageParts) + "."; @@ -2862,6 +2979,7 @@ public partial class DataCoupler : ComponentBase if (successCount > 0) messageParts.Add($"Inserimenti: {successCount}"); if (updatedCount > 0) messageParts.Add($"Aggiornamenti: {updatedCount}"); + if (skippedCount > 0) messageParts.Add($"Saltati: {skippedCount}"); if (duplicateCount > 0) messageParts.Add($"Duplicati (warning): {duplicateCount}"); messageParts.Add($"Errori: {errorCount}"); diff --git a/Data_Coupler/Pages/KeyAssociations.razor b/Data_Coupler/Pages/KeyAssociations.razor index 54452e9..f9d9992 100644 --- a/Data_Coupler/Pages/KeyAssociations.razor +++ b/Data_Coupler/Pages/KeyAssociations.razor @@ -242,6 +242,7 @@ Entità Destinazione ID Destinazione Credenziale + Hash Dati Stato Creata Verificata @@ -270,6 +271,20 @@ @association.RestCredentialName + + @if (!string.IsNullOrEmpty(association.Data_Hash)) + { + + @(association.Data_Hash.Substring(0, Math.Min(12, association.Data_Hash.Length)))... + + } + else + { + + Non disponibile + + } + @if (association.IsActive) {