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.
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
// <auto-generated />
|
||||
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
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AdditionalParameters")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("CommandTimeout")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(30);
|
||||
|
||||
b.Property<string>("ConnectionString")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DatabaseName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DatabaseType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedApiKey")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedAuthToken")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("EncryptedPassword")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Headers")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Host")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IgnoreSslErrors")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("Port")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("RestServiceType")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int>("TimeoutSeconds")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(100);
|
||||
|
||||
b.Property<string>("Type")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("TEXT")
|
||||
.HasDefaultValueSql("CURRENT_TIMESTAMP");
|
||||
|
||||
b.Property<string>("CreatedBy")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("DestinationCredentialId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("DestinationEndpoint")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationSchema")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationTable")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("FieldMappingJson")
|
||||
.HasMaxLength(4000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<DateTime?>("LastUsedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("SourceCredentialId")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("SourceCustomQuery")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceDatabaseName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceFilePath")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceKeyField")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceSchema")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceTable")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("AdditionalInfo")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data_Hash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationEntity")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationKeyField")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<string>("KeyValue")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("LastVerifiedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("RestCredentialName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourceKeyField")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("SourcesInfo")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace CredentialManager.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddDataHashField : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Data_Hash",
|
||||
table: "KeyAssociations",
|
||||
type: "TEXT",
|
||||
maxLength: 64,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Data_Hash",
|
||||
table: "KeyAssociations");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,6 +249,10 @@ namespace CredentialManager.Migrations
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("Data_Hash")
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("DestinationEntity")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
|
||||
@@ -88,4 +88,11 @@ public class KeyAssociation
|
||||
/// </summary>
|
||||
[MaxLength(2000)]
|
||||
public string? AdditionalInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hash SHA256 dei dati dei campi sorgente mappati.
|
||||
/// Utilizzato per rilevare cambiamenti nei dati sorgente e ottimizzare il trasferimento
|
||||
/// </summary>
|
||||
[MaxLength(64)]
|
||||
public string? Data_Hash { get; set; }
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private string GenerateDataHash(Dictionary<string, object> 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<string>();
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gestisce la connessione al database con schema specifico
|
||||
/// </summary>
|
||||
@@ -2313,19 +2372,6 @@ public partial class DataCoupler : ComponentBase
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private async Task<string> 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<string, object> transformedData, Dictionary<string, object> originalRecord, int recordNumber)>();
|
||||
var recordsForUpdate = new ConcurrentBag<(Dictionary<string, object> transformedData, string entityId, Dictionary<string, object> originalRecord, int recordNumber)>();
|
||||
var recordsForUpdate = new ConcurrentBag<(Dictionary<string, object> transformedData, string entityId, Dictionary<string, object> originalRecord, int recordNumber, string newDataHash)>();
|
||||
var recordsSkipped = new ConcurrentBag<(Dictionary<string, object> originalRecord, int recordNumber, string reason)>();
|
||||
var recordErrors = new ConcurrentBag<TransferResult>();
|
||||
|
||||
// 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
|
||||
{
|
||||
// Record da creare
|
||||
// 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 (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<DataConnection.REST.Implementations.SalesforceServiceClient.CompositeOperationResult>());
|
||||
@@ -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<string, object> originalRecord, string entityId, int recordNumber)
|
||||
private async Task CreateAssociationAsync(Dictionary<string, object> 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<string, object> 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}");
|
||||
|
||||
|
||||
@@ -242,6 +242,7 @@
|
||||
<th>Entità Destinazione</th>
|
||||
<th>ID Destinazione</th>
|
||||
<th>Credenziale</th>
|
||||
<th>Hash Dati</th>
|
||||
<th>Stato</th>
|
||||
<th>Creata</th>
|
||||
<th>Verificata</th>
|
||||
@@ -270,6 +271,20 @@
|
||||
<td>
|
||||
<span class="badge bg-secondary">@association.RestCredentialName</span>
|
||||
</td>
|
||||
<td>
|
||||
@if (!string.IsNullOrEmpty(association.Data_Hash))
|
||||
{
|
||||
<code class="small text-truncate d-inline-block" style="max-width: 120px;" title="@association.Data_Hash">
|
||||
@(association.Data_Hash.Substring(0, Math.Min(12, association.Data_Hash.Length)))...
|
||||
</code>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="text-muted">
|
||||
<i class="fas fa-minus"></i> Non disponibile
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (association.IsActive)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user