[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
+3
View File
@@ -289,6 +289,9 @@ public class ProfileScheduleBackup
[JsonPropertyName("createdBy")]
public string? CreatedBy { get; set; }
[JsonPropertyName("enableDeletionSync")]
public bool EnableDeletionSync { get; set; } = false;
}
/// <summary>
+4 -1
View File
@@ -1496,8 +1496,10 @@ public partial class DataCoupler : ComponentBase
recordNumber++;
}
// 3.5 Sincronizza le cancellazioni (se abilitato)
// 3.5 Sincronizzazione cancellazioni (DISABILITATA per trasferimenti manuali)
// Questa funzionalità è disponibile solo per le schedulazioni con configurazione esplicita
int deletedCount = 0;
/* DELETION SYNC DISABILITATA PER TRASFERIMENTI MANUALI
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKeyField))
{
try
@@ -1570,6 +1572,7 @@ public partial class DataCoupler : ComponentBase
});
}
}
*/
// 4. Mostra risultati
if (errorCount == 0)
+24
View File
@@ -336,6 +336,30 @@
</label>
</div>
<div class="card mb-3">
<div class="card-header bg-warning text-dark">
<h6 class="mb-0">
<i class="fas fa-exclamation-triangle"></i> Opzioni Avanzate
</h6>
</div>
<div class="card-body">
<div class="form-check mb-2">
<InputCheckbox @bind-Value="editingSchedule.EnableDeletionSync" class="form-check-input" id="enableDeletionSyncCheckbox" />
<label class="form-check-label" for="enableDeletionSyncCheckbox">
<strong>Abilita sincronizzazione eliminazioni</strong>
</label>
</div>
<div class="alert alert-warning mb-0">
<small>
<i class="fas fa-info-circle"></i>
<strong>Attenzione:</strong> Se abilitata, i record eliminati dalla sorgente saranno automaticamente eliminati anche dalla destinazione durante l'esecuzione schedulata.
Questa opzione è <strong>disabilitata di default</strong> per motivi di sicurezza.
Usare con cautela!
</small>
</div>
</div>
</div>
<div class="d-flex justify-content-end gap-2">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annulla</button>
<button type="submit" class="btn btn-primary">
@@ -21,4 +21,11 @@ public interface IScheduledProfileExecutionService
/// Esegue un profilo Data Coupler specificato dall'ID
/// </summary>
Task<ProfileExecutionResult> ExecuteProfileAsync(int profileId);
/// <summary>
/// Esegue un profilo Data Coupler specificato dall'ID con configurazione sincronizzazione eliminazioni
/// </summary>
/// <param name="profileId">ID del profilo da eseguire</param>
/// <param name="enableDeletionSync">Se true, sincronizza le eliminazioni dalla sorgente alla destinazione</param>
Task<ProfileExecutionResult> ExecuteProfileAsync(int profileId, bool enableDeletionSync);
}
@@ -69,11 +69,11 @@ public class ScheduledExecutionBackgroundService : BackgroundService
{
if (ShouldExecuteSchedule(schedule, currentTime))
{
_logger.LogInformation("Esecuzione schedulata per profilo: {ProfileName} (Schedule: {ScheduleName})",
schedule.Profile?.Name ?? "N/A", schedule.Name);
_logger.LogInformation("Esecuzione schedulata per profilo: {ProfileName} (Schedule: {ScheduleName}) - DeletionSync: {DeletionSync}",
schedule.Profile?.Name ?? "N/A", schedule.Name, schedule.EnableDeletionSync);
// Esegui il profilo
var result = await executionService.ExecuteProfileAsync(schedule.ProfileId);
// Esegui il profilo con il flag deletion sync dalla schedulazione
var result = await executionService.ExecuteProfileAsync(schedule.ProfileId, schedule.EnableDeletionSync);
// Aggiorna la schedulazione
await UpdateScheduleAfterExecution(scheduleService, schedule, currentTime, result.Success);
@@ -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)