[Feature] Disabilitata deletion sync nei trasferimenti manuali e aggiunta configurazione nelle schedulazioni

- Disabilitata completamente la sincronizzazione eliminazioni nei trasferimenti manuali (DataCoupler.razor.cs)
- Aggiunto campo EnableDeletionSync al modello ProfileSchedule (default: false)
- Implementata logica condizionale in ScheduledProfileExecutionService per deletion sync
- Aggiunta sezione 'Opzioni Avanzate' nell'interfaccia schedulazione con warning
- Creata migration Entity Framework AddEnableDeletionSyncToProfileSchedule
- Aggiornato BackupModels per supporto backup/restore del nuovo campo
- Aggiornata documentazione README.md e copilot-instructions.md
- La deletion sync è ora disponibile solo per schedulazioni con configurazione esplicita per massima sicurezza
This commit is contained in:
Alessio Dal Santo
2026-01-23 15:52:15 +01:00
parent 5169cd25c8
commit e35de1614f
13 changed files with 796 additions and 16 deletions
@@ -28,6 +28,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
private readonly IDataConnectionCredentialService _dataConnectionCredentialService;
private readonly IKeyAssociationService _keyAssociationService;
private readonly IAssociationService _associationService;
private readonly IDeletionSyncService _deletionSyncService;
private readonly ILogger<ScheduledProfileExecutionService> _logger;
public ScheduledProfileExecutionService(
@@ -37,6 +38,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
IDataConnectionCredentialService dataConnectionCredentialService,
IKeyAssociationService keyAssociationService,
IAssociationService associationService,
IDeletionSyncService deletionSyncService,
ILogger<ScheduledProfileExecutionService> logger)
{
_profileService = profileService;
@@ -45,6 +47,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
_dataConnectionCredentialService = dataConnectionCredentialService;
_keyAssociationService = keyAssociationService;
_associationService = associationService;
_deletionSyncService = deletionSyncService;
_logger = logger;
}
@@ -52,6 +55,14 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
/// Esegue un profilo Data Coupler specificato dall'ID
/// </summary>
public async Task<ProfileExecutionResult> ExecuteProfileAsync(int profileId)
{
return await ExecuteProfileAsync(profileId, enableDeletionSync: false);
}
/// <summary>
/// Esegue un profilo Data Coupler specificato dall'ID con configurazione sincronizzazione eliminazioni
/// </summary>
public async Task<ProfileExecutionResult> ExecuteProfileAsync(int profileId, bool enableDeletionSync)
{
var startTime = DateTime.UtcNow;
var result = new ProfileExecutionResult
@@ -61,7 +72,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
try
{
_logger.LogInformation("Inizio esecuzione profilo schedulato ID: {ProfileId}", profileId);
_logger.LogInformation("Inizio esecuzione profilo schedulato ID: {ProfileId} - DeletionSync: {DeletionSync}", profileId, enableDeletionSync);
// Carica il profilo
var profile = await _profileService.GetProfileByIdAsync(profileId);
@@ -78,7 +89,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
await _profileService.UpdateLastUsedAsync(profile.Id);
// Esegue il trasferimento dati con la logica completa
var recordsTransferred = await ExecuteDataTransferAsync(profile);
var recordsTransferred = await ExecuteDataTransferAsync(profile, enableDeletionSync);
result.Success = true;
result.RecordsProcessed = recordsTransferred;
@@ -106,10 +117,11 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
/// Metodo principale per l'esecuzione del trasferimento dati
/// Implementa la stessa logica di StartDataTransferWithComposite
/// </summary>
private async Task<int> ExecuteDataTransferAsync(DataCouplerProfile profile)
private async Task<int> ExecuteDataTransferAsync(DataCouplerProfile profile, bool enableDeletionSync = false)
{
_logger.LogInformation("=== INIZIO TRASFERIMENTO DATI SCHEDULATO ===");
_logger.LogInformation("Esecuzione profilo: {ProfileName} (ID: {ProfileId})", profile.Name, profile.Id);
_logger.LogInformation("Esecuzione profilo: {ProfileName} (ID: {ProfileId}) - DeletionSync: {DeletionSync}",
profile.Name, profile.Id, enableDeletionSync);
try
{
@@ -151,12 +163,12 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
if (useSalesforceComposite)
{
_logger.LogInformation("Utilizzo Salesforce Composite API per il trasferimento");
return await ExecuteDataTransferWithCompositeAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings);
return await ExecuteDataTransferWithCompositeAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, enableDeletionSync);
}
else
{
_logger.LogInformation("Utilizzo metodo trasferimento standard per il trasferimento");
return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings);
return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, enableDeletionSync);
}
}
catch (Exception ex)
@@ -389,9 +401,11 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
IRestServiceClient restClient,
RestEntitySummary restEntity,
RestApiCredential restCredential,
Dictionary<string, string> fieldMappings)
Dictionary<string, string> fieldMappings,
bool enableDeletionSync = false)
{
_logger.LogInformation("Iniziando trasferimento dati standard per {RecordCount} record", sourceRecords.Count());
_logger.LogInformation("Iniziando trasferimento dati standard per {RecordCount} record - DeletionSync: {DeletionSync}",
sourceRecords.Count(), enableDeletionSync);
int successCount = 0;
int errorCount = 0;
@@ -454,6 +468,50 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
_logger.LogInformation("Trasferimento completato. Successi: {SuccessCount}, Errori: {ErrorCount}",
successCount, errorCount);
// Sincronizzazione cancellazioni (se abilitata)
if (enableDeletionSync && profile.UseRecordAssociations && !string.IsNullOrEmpty(profile.SourceKeyField))
{
try
{
_logger.LogInformation("SCHEDULED: Inizio sincronizzazione cancellazioni...");
// Estrai tutti i valori chiave presenti nella sorgente
var sourceKeyValues = sourceRecords
.Select(r => r.ContainsKey(profile.SourceKeyField) ? r[profile.SourceKeyField]?.ToString() : null)
.Where(k => !string.IsNullOrEmpty(k))
.Cast<string>()
.Distinct()
.ToList();
_logger.LogInformation("SCHEDULED: Trovati {Count} valori chiave nella sorgente", sourceKeyValues.Count);
// Sincronizza le cancellazioni
var deletionOptions = new DeletionSyncOptions
{
Action = DeletionAction.Delete // Default: elimina fisicamente
};
var deletionResult = await _deletionSyncService.SyncDeletionsAsync(
sourceKeyValues,
restEntity.Name,
restCredential.Name,
restClient,
deletionOptions);
if (deletionResult.DeletedRecordsDetected > 0)
{
_logger.LogInformation("SCHEDULED: Sincronizzazione cancellazioni completata - {Detected} rilevati, {Synced} sincronizzati, {Errors} errori",
deletionResult.DeletedRecordsDetected,
deletionResult.DeletedRecordsSynced,
deletionResult.SyncErrors);
}
}
catch (Exception delEx)
{
_logger.LogError(delEx, "SCHEDULED: Errore durante la sincronizzazione delle cancellazioni");
}
}
return successCount;
}
@@ -467,15 +525,17 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
IRestServiceClient restClient,
RestEntitySummary restEntity,
RestApiCredential restCredential,
Dictionary<string, string> fieldMappings)
Dictionary<string, string> fieldMappings,
bool enableDeletionSync = false)
{
_logger.LogInformation("Iniziando trasferimento dati COMPOSITE per {RecordCount} record", sourceRecords.Count());
_logger.LogInformation("Iniziando trasferimento dati COMPOSITE per {RecordCount} record - DeletionSync: {DeletionSync}",
sourceRecords.Count(), enableDeletionSync);
// Verifica che sia effettivamente un SalesforceServiceClient
if (!(restClient is DataConnection.REST.Implementations.SalesforceServiceClient salesforceClient))
{
_logger.LogWarning("Client REST non è SalesforceServiceClient, fallback al metodo standard");
return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential, fieldMappings);
return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential, fieldMappings, enableDeletionSync);
}
try
@@ -738,6 +798,50 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
_logger.LogInformation("COMPOSITE SCHEDULED: Trasferimento completato. Creazioni: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Saltati: {SkippedCount}, Errori: {ErrorCount}",
successCount, updatedCount, skippedCount, errorCount);
// Sincronizzazione cancellazioni (se abilitata)
if (enableDeletionSync && currentUseRecordAssociations && !string.IsNullOrEmpty(profile.SourceKeyField))
{
try
{
_logger.LogInformation("COMPOSITE SCHEDULED: Inizio sincronizzazione cancellazioni...");
// Estrai tutti i valori chiave presenti nella sorgente
var sourceKeyValues = sourceRecords
.Select(r => r.ContainsKey(profile.SourceKeyField) ? r[profile.SourceKeyField]?.ToString() : null)
.Where(k => !string.IsNullOrEmpty(k))
.Cast<string>()
.Distinct()
.ToList();
_logger.LogInformation("COMPOSITE SCHEDULED: Trovati {Count} valori chiave nella sorgente", sourceKeyValues.Count);
// Sincronizza le cancellazioni
var deletionOptions = new DeletionSyncOptions
{
Action = DeletionAction.Delete // Default: elimina fisicamente
};
var deletionResult = await _deletionSyncService.SyncDeletionsAsync(
sourceKeyValues,
currentEntityName,
currentCredentialName,
restClient,
deletionOptions);
if (deletionResult.DeletedRecordsDetected > 0)
{
_logger.LogInformation("COMPOSITE SCHEDULED: Sincronizzazione cancellazioni completata - {Detected} rilevati, {Synced} sincronizzati, {Errors} errori",
deletionResult.DeletedRecordsDetected,
deletionResult.DeletedRecordsSynced,
deletionResult.SyncErrors);
}
}
catch (Exception delEx)
{
_logger.LogError(delEx, "COMPOSITE SCHEDULED: Errore durante la sincronizzazione delle cancellazioni");
}
}
return totalProcessed;
}
catch (Exception ex)