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")
|
b.Property<DateTime>("CreatedAt")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Data_Hash")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("DestinationEntity")
|
b.Property<string>("DestinationEntity")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasMaxLength(200)
|
.HasMaxLength(200)
|
||||||
|
|||||||
@@ -88,4 +88,11 @@ public class KeyAssociation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[MaxLength(2000)]
|
[MaxLength(2000)]
|
||||||
public string? AdditionalInfo { get; set; }
|
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 sourceKeyField = association.SourceKeyField;
|
||||||
var destinationKeyField = association.DestinationKeyField;
|
var destinationKeyField = association.DestinationKeyField;
|
||||||
var additionalInfo = association.AdditionalInfo;
|
var additionalInfo = association.AdditionalInfo;
|
||||||
|
var dataHash = association.Data_Hash;
|
||||||
var currentTime = DateTime.UtcNow;
|
var currentTime = DateTime.UtcNow;
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -47,12 +48,13 @@ public class KeyAssociationService : IKeyAssociationService
|
|||||||
DestinationKeyField = {2},
|
DestinationKeyField = {2},
|
||||||
UpdatedAt = {3},
|
UpdatedAt = {3},
|
||||||
LastVerifiedAt = {4},
|
LastVerifiedAt = {4},
|
||||||
AdditionalInfo = {5}
|
AdditionalInfo = {5},
|
||||||
WHERE KeyValue = {6}
|
Data_Hash = {6}
|
||||||
AND DestinationEntity = {7}
|
WHERE KeyValue = {7}
|
||||||
AND RestCredentialName = {8}
|
AND DestinationEntity = {8}
|
||||||
|
AND RestCredentialName = {9}
|
||||||
AND IsActive = 1",
|
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);
|
keyValue, destinationEntity, restCredentialName);
|
||||||
|
|
||||||
if (rowsAffected > 0)
|
if (rowsAffected > 0)
|
||||||
@@ -92,6 +94,7 @@ public class KeyAssociationService : IKeyAssociationService
|
|||||||
CreatedAt = currentTime,
|
CreatedAt = currentTime,
|
||||||
LastVerifiedAt = currentTime,
|
LastVerifiedAt = currentTime,
|
||||||
AdditionalInfo = additionalInfo,
|
AdditionalInfo = additionalInfo,
|
||||||
|
Data_Hash = dataHash,
|
||||||
IsActive = true
|
IsActive = true
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -125,6 +128,7 @@ public class KeyAssociationService : IKeyAssociationService
|
|||||||
existing.UpdatedAt = currentTime;
|
existing.UpdatedAt = currentTime;
|
||||||
existing.LastVerifiedAt = currentTime;
|
existing.LastVerifiedAt = currentTime;
|
||||||
existing.AdditionalInfo = additionalInfo;
|
existing.AdditionalInfo = additionalInfo;
|
||||||
|
existing.Data_Hash = dataHash;
|
||||||
|
|
||||||
UpdateSourcesInfo(existing, association);
|
UpdateSourcesInfo(existing, association);
|
||||||
|
|
||||||
@@ -162,6 +166,7 @@ public class KeyAssociationService : IKeyAssociationService
|
|||||||
var sourceKeyField = association.SourceKeyField;
|
var sourceKeyField = association.SourceKeyField;
|
||||||
var destinationKeyField = association.DestinationKeyField;
|
var destinationKeyField = association.DestinationKeyField;
|
||||||
var additionalInfo = association.AdditionalInfo;
|
var additionalInfo = association.AdditionalInfo;
|
||||||
|
var dataHash = association.Data_Hash;
|
||||||
var currentTime = DateTime.UtcNow;
|
var currentTime = DateTime.UtcNow;
|
||||||
|
|
||||||
// Crea un nuovo DbContext per questa operazione parallela
|
// Crea un nuovo DbContext per questa operazione parallela
|
||||||
@@ -185,12 +190,13 @@ public class KeyAssociationService : IKeyAssociationService
|
|||||||
DestinationKeyField = {2},
|
DestinationKeyField = {2},
|
||||||
UpdatedAt = {3},
|
UpdatedAt = {3},
|
||||||
LastVerifiedAt = {4},
|
LastVerifiedAt = {4},
|
||||||
AdditionalInfo = {5}
|
AdditionalInfo = {5},
|
||||||
WHERE KeyValue = {6}
|
Data_Hash = {6}
|
||||||
AND DestinationEntity = {7}
|
WHERE KeyValue = {7}
|
||||||
AND RestCredentialName = {8}
|
AND DestinationEntity = {8}
|
||||||
|
AND RestCredentialName = {9}
|
||||||
AND IsActive = 1",
|
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);
|
keyValue, destinationEntity, restCredentialName);
|
||||||
|
|
||||||
if (rowsAffected > 0)
|
if (rowsAffected > 0)
|
||||||
@@ -230,6 +236,7 @@ public class KeyAssociationService : IKeyAssociationService
|
|||||||
CreatedAt = currentTime,
|
CreatedAt = currentTime,
|
||||||
LastVerifiedAt = currentTime,
|
LastVerifiedAt = currentTime,
|
||||||
AdditionalInfo = additionalInfo,
|
AdditionalInfo = additionalInfo,
|
||||||
|
Data_Hash = dataHash,
|
||||||
IsActive = true
|
IsActive = true
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -549,9 +556,11 @@ public class KeyAssociationService : IKeyAssociationService
|
|||||||
existing.DestinationId = association.DestinationId;
|
existing.DestinationId = association.DestinationId;
|
||||||
existing.RestCredentialName = association.RestCredentialName;
|
existing.RestCredentialName = association.RestCredentialName;
|
||||||
existing.UpdatedAt = DateTime.UtcNow;
|
existing.UpdatedAt = DateTime.UtcNow;
|
||||||
|
existing.LastVerifiedAt = association.LastVerifiedAt;
|
||||||
existing.AdditionalInfo = association.AdditionalInfo;
|
existing.AdditionalInfo = association.AdditionalInfo;
|
||||||
existing.SourcesInfo = association.SourcesInfo;
|
existing.SourcesInfo = association.SourcesInfo;
|
||||||
existing.IsActive = association.IsActive;
|
existing.IsActive = association.IsActive;
|
||||||
|
existing.Data_Hash = association.Data_Hash;
|
||||||
|
|
||||||
_context.KeyAssociations.Update(existing);
|
_context.KeyAssociations.Update(existing);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|||||||
Binary file not shown.
@@ -1727,6 +1727,7 @@ public partial class DataCoupler : ComponentBase
|
|||||||
"success" => "",
|
"success" => "",
|
||||||
"updated" => "table-info",
|
"updated" => "table-info",
|
||||||
"duplicate" => "table-warning",
|
"duplicate" => "table-warning",
|
||||||
|
"skipped" => "table-secondary",
|
||||||
"error" => "table-danger",
|
"error" => "table-danger",
|
||||||
_ => ""
|
_ => ""
|
||||||
};
|
};
|
||||||
@@ -1739,6 +1740,7 @@ public partial class DataCoupler : ComponentBase
|
|||||||
"success" => "bg-success",
|
"success" => "bg-success",
|
||||||
"updated" => "bg-info",
|
"updated" => "bg-info",
|
||||||
"duplicate" => "bg-warning text-dark",
|
"duplicate" => "bg-warning text-dark",
|
||||||
|
"skipped" => "bg-secondary",
|
||||||
"error" => "bg-danger",
|
"error" => "bg-danger",
|
||||||
_ => "bg-secondary"
|
_ => "bg-secondary"
|
||||||
};
|
};
|
||||||
@@ -1751,6 +1753,7 @@ public partial class DataCoupler : ComponentBase
|
|||||||
"success" => "fa-check-circle",
|
"success" => "fa-check-circle",
|
||||||
"updated" => "fa-edit",
|
"updated" => "fa-edit",
|
||||||
"duplicate" => "fa-exclamation-triangle",
|
"duplicate" => "fa-exclamation-triangle",
|
||||||
|
"skipped" => "fa-forward",
|
||||||
"error" => "fa-times-circle",
|
"error" => "fa-times-circle",
|
||||||
_ => "fa-question-circle"
|
_ => "fa-question-circle"
|
||||||
};
|
};
|
||||||
@@ -1763,6 +1766,7 @@ public partial class DataCoupler : ComponentBase
|
|||||||
"success" => "Inserito",
|
"success" => "Inserito",
|
||||||
"updated" => "Aggiornato",
|
"updated" => "Aggiornato",
|
||||||
"duplicate" => "Duplicato",
|
"duplicate" => "Duplicato",
|
||||||
|
"skipped" => "Saltato",
|
||||||
"error" => "Errore",
|
"error" => "Errore",
|
||||||
_ => "Sconosciuto"
|
_ => "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>
|
/// <summary>
|
||||||
/// Gestisce la connessione al database con schema specifico
|
/// Gestisce la connessione al database con schema specifico
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2313,19 +2372,6 @@ public partial class DataCoupler : ComponentBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private async Task<string> GenerateUniqueProfileName(string baseName)
|
private async Task<string> GenerateUniqueProfileName(string baseName)
|
||||||
{
|
{
|
||||||
var uniqueName = baseName;
|
var uniqueName = baseName;
|
||||||
@@ -2499,7 +2545,8 @@ public partial class DataCoupler : ComponentBase
|
|||||||
|
|
||||||
// 2. Trasforma i record e analizza le associazioni IN PARALLELO
|
// 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 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>();
|
var recordErrors = new ConcurrentBag<TransferResult>();
|
||||||
|
|
||||||
// Cattura i valori condivisi per evitare race conditions
|
// 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)
|
// Trasforma il record in base ai mapping (operazione locale, thread-safe)
|
||||||
var restData = TransformRecordToRestEntity(record);
|
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 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))
|
if (currentUseRecordAssociations && !string.IsNullOrEmpty(sourceKey))
|
||||||
{
|
{
|
||||||
Logger.LogDebug("COMPOSITE PARALLEL: Cerco associazione per KeyValue: '{KeyValue}', Entity: '{Entity}', Credential: '{Credential}'",
|
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)
|
if (existingAssociation != null && existingAssociation.IsActive)
|
||||||
{
|
{
|
||||||
// Record da aggiornare
|
// CONTROLLO HASH: Verifica se i dati sono cambiati
|
||||||
recordsForUpdate.Add((restData, existingAssociation.DestinationId, record, recordNumber));
|
var existingHash = existingAssociation.Data_Hash;
|
||||||
Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} marcato per aggiornamento (EntityId: {EntityId})",
|
|
||||||
recordNumber, existingAssociation.DestinationId);
|
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
|
else
|
||||||
{
|
{
|
||||||
// Record da creare
|
// Record da creare (nessuna associazione esistente)
|
||||||
recordsForCreate.Add((restData, record, recordNumber));
|
recordsForCreate.Add((restData, record, recordNumber));
|
||||||
Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} marcato per creazione", 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
|
// Converti i ConcurrentBag in liste per il resto del processing
|
||||||
var finalRecordsForCreate = recordsForCreate.ToList();
|
var finalRecordsForCreate = recordsForCreate.ToList();
|
||||||
var finalRecordsForUpdate = recordsForUpdate.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",
|
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, recordErrors.Count);
|
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
|
// 3. Esegui le chiamate composite in parallelo
|
||||||
var createTask = Task.FromResult(new List<DataConnection.REST.Implementations.SalesforceServiceClient.CompositeOperationResult>());
|
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))
|
if (useRecordAssociations && !string.IsNullOrEmpty(transferResult.EntityId))
|
||||||
{
|
{
|
||||||
// IMPORTANTE: Non awaita qui, solo crea il task per esecuzione parallela
|
// 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);
|
createAssociationTasks.Add(associationTask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2699,8 +2775,8 @@ public partial class DataCoupler : ComponentBase
|
|||||||
if (useRecordAssociations && !string.IsNullOrEmpty(result.EntityId))
|
if (useRecordAssociations && !string.IsNullOrEmpty(result.EntityId))
|
||||||
{
|
{
|
||||||
// IMPORTANTE: Non awaita qui, solo crea il task per esecuzione parallela
|
// IMPORTANTE: Non awaita qui, solo crea il task per esecuzione parallela
|
||||||
var verificationTask = UpdateAssociationVerificationAsync(result.EntityId);
|
var updateHashTask = UpdateAssociationHashAsync(originalData.originalRecord, result.EntityId, originalData.newDataHash);
|
||||||
updateAssociationTasks.Add(verificationTask);
|
updateAssociationTasks.Add(updateHashTask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -2709,13 +2785,9 @@ public partial class DataCoupler : ComponentBase
|
|||||||
transferResult.Status = "error";
|
transferResult.Status = "error";
|
||||||
transferResult.Message = $"Errore aggiornamento (Composite): {result.ErrorMessage}";
|
transferResult.Message = $"Errore aggiornamento (Composite): {result.ErrorMessage}";
|
||||||
|
|
||||||
// Aggiungi task di gestione fallimento alla lista (esecuzione parallela)
|
// NON aggiornare l'hash in caso di errore nel trasferimento
|
||||||
if (useRecordAssociations)
|
Logger.LogWarning("COMPOSITE: Trasferimento fallito per record {RecordNumber} - EntityId: {EntityId}. Hash non aggiornato.",
|
||||||
{
|
originalData.recordNumber, result.EntityId ?? "N/A");
|
||||||
// IMPORTANTE: Non awaita qui, solo crea il task per esecuzione parallela
|
|
||||||
var failureTask = HandleFailedUpdateAsync(originalData.originalRecord, originalData.recordNumber);
|
|
||||||
updateAssociationTasks.Add(failureTask);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
transferResults.Add(transferResult);
|
transferResults.Add(transferResult);
|
||||||
@@ -2740,11 +2812,12 @@ public partial class DataCoupler : ComponentBase
|
|||||||
Logger.LogInformation("COMPOSITE: Nessuna operazione di associazione da eseguire");
|
Logger.LogInformation("COMPOSITE: Nessuna operazione di associazione da eseguire");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Mostra risultati
|
// 7. Mostra risultati (inclusi i record saltati)
|
||||||
ShowTransferResults(successCount, updatedCount, 0, errorCount);
|
var skippedCount = finalRecordsSkipped.Count;
|
||||||
|
ShowTransferResults(successCount, updatedCount, 0, errorCount, skippedCount);
|
||||||
|
|
||||||
Logger.LogInformation("Trasferimento COMPOSITE completato. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Errori: {ErrorCount}",
|
Logger.LogInformation("Trasferimento COMPOSITE completato. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Saltati: {SkippedCount}, Errori: {ErrorCount}",
|
||||||
successCount, updatedCount, errorCount);
|
successCount, updatedCount, skippedCount, errorCount);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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
|
try
|
||||||
{
|
{
|
||||||
@@ -2772,6 +2845,9 @@ public partial class DataCoupler : ComponentBase
|
|||||||
var sourceKey = GenerateSourceKey(originalRecord);
|
var sourceKey = GenerateSourceKey(originalRecord);
|
||||||
if (string.IsNullOrEmpty(sourceKey)) return;
|
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 destinationKeyField = GetEntityIdField();
|
||||||
var association = new KeyAssociation
|
var association = new KeyAssociation
|
||||||
{
|
{
|
||||||
@@ -2783,18 +2859,21 @@ public partial class DataCoupler : ComponentBase
|
|||||||
RestCredentialName = currentCredentialName,
|
RestCredentialName = currentCredentialName,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
LastVerifiedAt = DateTime.UtcNow,
|
LastVerifiedAt = DateTime.UtcNow,
|
||||||
|
Data_Hash = finalDataHash,
|
||||||
AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new
|
AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new
|
||||||
{
|
{
|
||||||
TransferDate = DateTime.UtcNow,
|
TransferDate = DateTime.UtcNow,
|
||||||
RecordNumber = recordNumber,
|
RecordNumber = recordNumber,
|
||||||
MappingCount = currentMappingCount,
|
MappingCount = currentMappingCount,
|
||||||
SourceType = currentSourceType,
|
SourceType = currentSourceType,
|
||||||
CompositeTransfer = true
|
CompositeTransfer = true,
|
||||||
|
DataHashGenerated = true
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
var associationId = await CredentialService.SaveKeyAssociationParallelAsync(association);
|
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)
|
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)
|
private Task UpdateAssociationVerificationAsync(string entityId)
|
||||||
{
|
{
|
||||||
try
|
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)
|
if (errorCount == 0)
|
||||||
{
|
{
|
||||||
@@ -2849,6 +2965,7 @@ public partial class DataCoupler : ComponentBase
|
|||||||
|
|
||||||
if (successCount > 0) messageParts.Add($"{successCount} record inseriti");
|
if (successCount > 0) messageParts.Add($"{successCount} record inseriti");
|
||||||
if (updatedCount > 0) messageParts.Add($"{updatedCount} record aggiornati");
|
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)");
|
if (duplicateCount > 0) messageParts.Add($"{duplicateCount} duplicati rilevati (warning)");
|
||||||
|
|
||||||
message += string.Join(", ", messageParts) + ".";
|
message += string.Join(", ", messageParts) + ".";
|
||||||
@@ -2862,6 +2979,7 @@ public partial class DataCoupler : ComponentBase
|
|||||||
|
|
||||||
if (successCount > 0) messageParts.Add($"Inserimenti: {successCount}");
|
if (successCount > 0) messageParts.Add($"Inserimenti: {successCount}");
|
||||||
if (updatedCount > 0) messageParts.Add($"Aggiornamenti: {updatedCount}");
|
if (updatedCount > 0) messageParts.Add($"Aggiornamenti: {updatedCount}");
|
||||||
|
if (skippedCount > 0) messageParts.Add($"Saltati: {skippedCount}");
|
||||||
if (duplicateCount > 0) messageParts.Add($"Duplicati (warning): {duplicateCount}");
|
if (duplicateCount > 0) messageParts.Add($"Duplicati (warning): {duplicateCount}");
|
||||||
messageParts.Add($"Errori: {errorCount}");
|
messageParts.Add($"Errori: {errorCount}");
|
||||||
|
|
||||||
|
|||||||
@@ -242,6 +242,7 @@
|
|||||||
<th>Entità Destinazione</th>
|
<th>Entità Destinazione</th>
|
||||||
<th>ID Destinazione</th>
|
<th>ID Destinazione</th>
|
||||||
<th>Credenziale</th>
|
<th>Credenziale</th>
|
||||||
|
<th>Hash Dati</th>
|
||||||
<th>Stato</th>
|
<th>Stato</th>
|
||||||
<th>Creata</th>
|
<th>Creata</th>
|
||||||
<th>Verificata</th>
|
<th>Verificata</th>
|
||||||
@@ -270,6 +271,20 @@
|
|||||||
<td>
|
<td>
|
||||||
<span class="badge bg-secondary">@association.RestCredentialName</span>
|
<span class="badge bg-secondary">@association.RestCredentialName</span>
|
||||||
</td>
|
</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>
|
<td>
|
||||||
@if (association.IsActive)
|
@if (association.IsActive)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user