Files
Data-Coupler/Data_Coupler/Services/DataTransferService.cs
T
Alessio d042863a56 feat: Implementazione completa sistema schedulazione con intervalli personalizzati
- Aggiunto supporto schedulazione con intervalli flessibili (secondi/minuti/ore/giorni/settimane/mesi)
- Esteso modello ProfileSchedule con campi IntervalValue e IntervalUnit
- Ottimizzato ScheduledJobService per controlli ogni 30s con esecuzione parallela
- Implementata interfaccia UI completa con anteprima real-time in italiano
- Aggiunta migrazione database AddIntervalSchedulingFields
- Implementati metodi calcolo NextExecutionTime per intervalli
- Aggiunta gestione tracking anti-duplicati e cleanup automatico
- Creata documentazione completa (6 file, 2500+ righe)

Modifiche tecniche:
- ProfileSchedule.cs: Nuovi campi e metodi CalculateNextInterval/GetScheduleDescription
- ScheduledJobService.cs: Ridotto check interval a 30s, aggiunto parallel processing
- ProfileScheduleService.cs: Supporto calcolo intervalli in UpdateNextExecutionTimeAsync
- Scheduling.razor: Aggiunta sezione UI per configurazione intervalli
- Scheduling.razor.cs: Implementato GetIntervalPreview() e gestione stato campi
2025-10-02 01:12:39 +02:00

210 lines
9.1 KiB
C#

using CredentialManager.Models;
using Microsoft.Extensions.Logging;
namespace Data_Coupler.Services;
/// <summary>
/// Servizio per l'esecuzione automatica dei trasferimenti dati basati sui profili
/// </summary>
public interface IDataTransferService
{
Task<DataTransferResult> ExecuteProfileAsync(DataCouplerProfile profile, string? sourceDatabaseOverride = null, string? destinationDatabaseOverride = null);
}
public class DataTransferResult
{
public bool IsSuccess { get; set; }
public int RecordsProcessed { get; set; }
public string? ErrorMessage { get; set; }
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public List<string> ErrorDetails { get; set; } = new();
public Dictionary<string, object> AdditionalInfo { get; set; } = new();
public TimeSpan Duration => EndTime - StartTime;
}
public class DataTransferService : IDataTransferService
{
private readonly IScheduledProfileExecutionService _scheduledExecutionService;
private readonly ILogger<DataTransferService> _logger;
public DataTransferService(
IScheduledProfileExecutionService scheduledExecutionService,
ILogger<DataTransferService> logger)
{
_scheduledExecutionService = scheduledExecutionService ?? throw new ArgumentNullException(nameof(scheduledExecutionService));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<DataTransferResult> ExecuteProfileAsync(DataCouplerProfile profile, string? sourceDatabaseOverride = null, string? destinationDatabaseOverride = null)
{
var result = new DataTransferResult
{
StartTime = DateTime.Now // Usa l'ora locale per coerenza con le schedulazioni
};
try
{
_logger.LogInformation("Iniziando esecuzione profilo {ProfileName} (ID: {ProfileId})",
profile.Name, profile.Id);
// Validazione del profilo
var validationResult = ValidateProfile(profile);
if (!validationResult.IsValid)
{
result.IsSuccess = false;
result.ErrorMessage = validationResult.ErrorMessage;
result.EndTime = DateTime.Now; // Usa l'ora locale per coerenza
return result;
}
// Controlla se il profilo ha file come sorgente e blocca l'esecuzione
if (profile.SourceType?.ToLower() == "file")
{
result.IsSuccess = false;
result.ErrorMessage = "I profili con file come sorgente non sono supportati nelle schedulazioni per motivi di sicurezza.";
result.EndTime = DateTime.Now; // Usa l'ora locale per coerenza
_logger.LogWarning("Tentativo di esecuzione di profilo con file come sorgente bloccato: {ProfileName}", profile.Name);
return result;
}
// Applica override del database se specificati
var profileToExecute = await ApplyDatabaseOverrides(profile, sourceDatabaseOverride, destinationDatabaseOverride);
// Utilizza il servizio esistente per l'esecuzione
var executionResult = await _scheduledExecutionService.ExecuteProfileAsync(profileToExecute.Id);
result.IsSuccess = executionResult.Success;
result.RecordsProcessed = executionResult.RecordsProcessed;
result.ErrorMessage = executionResult.Success ? null : executionResult.Message;
result.EndTime = DateTime.Now; // Usa l'ora locale per coerenza
if (executionResult.Success)
{
result.AdditionalInfo["ExecutionDuration"] = executionResult.Duration.ToString();
_logger.LogInformation("Profilo {ProfileName} eseguito con successo. " +
"Record processati: {RecordsProcessed}, Durata: {Duration}ms",
profile.Name, result.RecordsProcessed, result.Duration.TotalMilliseconds);
}
else
{
result.ErrorDetails.Add(executionResult.Message);
_logger.LogError("Errore nell'esecuzione del profilo {ProfileName}: {ErrorMessage}",
profile.Name, executionResult.Message);
}
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore durante l'esecuzione del profilo {ProfileName} (ID: {ProfileId})",
profile.Name, profile.Id);
result.IsSuccess = false;
result.ErrorMessage = ex.Message;
result.ErrorDetails.Add($"Exception: {ex.GetType().Name} - {ex.Message}");
if (ex.InnerException != null)
{
result.ErrorDetails.Add($"Inner Exception: {ex.InnerException.Message}");
}
result.EndTime = DateTime.Now; // Usa l'ora locale per coerenza
return result;
}
}
private async Task<DataCouplerProfile> ApplyDatabaseOverrides(DataCouplerProfile originalProfile,
string? sourceDatabaseOverride, string? destinationDatabaseOverride)
{
// Se non ci sono override, restituisce il profilo originale
if (string.IsNullOrEmpty(sourceDatabaseOverride) && string.IsNullOrEmpty(destinationDatabaseOverride))
{
return originalProfile;
}
// Crea una copia del profilo con gli override applicati
// In un'implementazione reale, potresti voler creare una copia più sofisticata
// o utilizzare un metodo di clonazione appropriato
var profileCopy = new DataCouplerProfile
{
Id = originalProfile.Id,
Name = originalProfile.Name,
Description = originalProfile.Description,
SourceType = originalProfile.SourceType,
SourceCredentialId = originalProfile.SourceCredentialId,
SourceDatabaseName = originalProfile.SourceDatabaseName,
SourceTable = originalProfile.SourceTable,
SourceSchema = originalProfile.SourceSchema,
SourceCustomQuery = originalProfile.SourceCustomQuery,
SourceFilePath = originalProfile.SourceFilePath,
DestinationType = originalProfile.DestinationType,
DestinationCredentialId = originalProfile.DestinationCredentialId,
DestinationTable = originalProfile.DestinationTable,
DestinationSchema = originalProfile.DestinationSchema,
DestinationEndpoint = originalProfile.DestinationEndpoint,
FieldMappingJson = originalProfile.FieldMappingJson,
SourceKeyField = originalProfile.SourceKeyField,
UseRecordAssociations = originalProfile.UseRecordAssociations,
IsActive = originalProfile.IsActive,
CreatedAt = originalProfile.CreatedAt,
LastUsedAt = originalProfile.LastUsedAt,
CreatedBy = originalProfile.CreatedBy
};
// TODO: Implementare l'applicazione degli override del database
// Questo richiederebbe di modificare temporaneamente la stringa di connessione
// delle credenziali per puntare al database specificato
_logger.LogInformation("Applicazione override database - Source: {SourceDB}, Destination: {DestDB}",
sourceDatabaseOverride ?? "none", destinationDatabaseOverride ?? "none");
return await Task.FromResult(profileCopy);
}
private (bool IsValid, string? ErrorMessage) ValidateProfile(DataCouplerProfile profile)
{
if (profile == null)
return (false, "Profilo non specificato");
if (string.IsNullOrEmpty(profile.Name))
return (false, "Nome profilo non specificato");
if (string.IsNullOrEmpty(profile.SourceType))
return (false, "Tipo sorgente non specificato");
if (string.IsNullOrEmpty(profile.DestinationType))
return (false, "Tipo destinazione non specificato");
if (!profile.SourceCredentialId.HasValue)
return (false, "Credenziale sorgente non specificata");
if (!profile.DestinationCredentialId.HasValue)
return (false, "Credenziale destinazione non specificata");
// Validazioni specifiche per tipo sorgente
if (profile.SourceType == "database")
{
if (string.IsNullOrEmpty(profile.SourceTable) && string.IsNullOrEmpty(profile.SourceCustomQuery))
return (false, "Tabella sorgente o query personalizzata deve essere specificata");
}
else if (profile.SourceType == "file")
{
if (string.IsNullOrEmpty(profile.SourceFilePath))
return (false, "Percorso file sorgente non specificato");
}
// Validazioni specifiche per tipo destinazione
if (profile.DestinationType == "database")
{
if (string.IsNullOrEmpty(profile.DestinationTable))
return (false, "Tabella destinazione non specificata");
}
else if (profile.DestinationType == "rest")
{
if (string.IsNullOrEmpty(profile.DestinationEndpoint))
return (false, "Endpoint REST destinazione non specificato");
}
return (true, null);
}
}