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
This commit is contained in:
2025-10-02 01:12:39 +02:00
parent b76a6760fb
commit d042863a56
71 changed files with 17860 additions and 144 deletions
+733
View File
@@ -0,0 +1,733 @@
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using CredentialManager.Data;
using CredentialManager.Models;
using CredentialManager.Services;
using Data_Coupler.Models;
namespace Data_Coupler.Services;
/// <summary>
/// Interfaccia per il servizio di backup e ripristino
/// </summary>
public interface IBackupService
{
/// <summary>
/// Esporta tutti i dati del sistema in un file di backup
/// </summary>
Task<BackupOperationResult> ExportBackupAsync(BackupOptions options);
/// <summary>
/// Importa i dati da un file di backup
/// </summary>
Task<BackupOperationResult> ImportBackupAsync(string backupFilePath, RestoreOptions options);
/// <summary>
/// Importa i dati da contenuto JSON
/// </summary>
Task<BackupOperationResult> ImportBackupFromJsonAsync(string jsonContent, RestoreOptions options);
/// <summary>
/// Valida un file di backup
/// </summary>
Task<BackupOperationResult> ValidateBackupAsync(string backupFilePath);
/// <summary>
/// Ottiene le informazioni su un backup senza importarlo
/// </summary>
Task<SystemBackupData?> GetBackupInfoAsync(string backupFilePath);
}
/// <summary>
/// Servizio per la gestione dei backup e ripristini del sistema
/// </summary>
public class BackupService : IBackupService
{
private readonly CredentialDbContext _context;
private readonly IDataCouplerProfileService _profileService;
private readonly ICredentialService _credentialService;
private readonly IKeyAssociationService _keyAssociationService;
private readonly ILogger<BackupService> _logger;
public BackupService(
CredentialDbContext context,
IDataCouplerProfileService profileService,
ICredentialService credentialService,
IKeyAssociationService keyAssociationService,
ILogger<BackupService> logger)
{
_context = context;
_profileService = profileService;
_credentialService = credentialService;
_keyAssociationService = keyAssociationService;
_logger = logger;
}
/// <summary>
/// Esporta tutti i dati del sistema
/// </summary>
public async Task<BackupOperationResult> ExportBackupAsync(BackupOptions options)
{
var result = new BackupOperationResult();
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
_logger.LogInformation("Avvio backup con opzioni: {@Options}", options);
var backupData = new SystemBackupData
{
Metadata = new BackupMetadata
{
CreatedAt = DateTime.UtcNow,
CreatedBy = options.CreatedBy,
Description = options.Description,
ApplicationVersion = "1.0.0"
}
};
// Export Profiles
if (options.IncludeProfiles)
{
_logger.LogDebug("Esportazione profili in corso...");
backupData.Profiles = await ExportProfilesAsync(options.IncludeOnlyActiveRecords);
result.ProcessedCounts.Profiles = backupData.Profiles.Count;
_logger.LogDebug("Esportati {Count} profili", backupData.Profiles.Count);
}
// Export Credentials (senza dati sensibili)
if (options.IncludeCredentials)
{
_logger.LogDebug("Esportazione credenziali in corso...");
backupData.Credentials = await ExportCredentialsAsync(options.IncludeOnlyActiveRecords);
result.ProcessedCounts.Credentials = backupData.Credentials.Count;
result.Warnings.Add("Le credenziali esportate non includono password, API keys o token per motivi di sicurezza");
_logger.LogDebug("Esportate {Count} credenziali", backupData.Credentials.Count);
}
// Export Key Associations
if (options.IncludeKeyAssociations)
{
_logger.LogDebug("Esportazione associazioni chiavi in corso...");
backupData.KeyAssociations = await ExportKeyAssociationsAsync(options.IncludeOnlyActiveRecords);
result.ProcessedCounts.KeyAssociations = backupData.KeyAssociations.Count;
_logger.LogDebug("Esportate {Count} associazioni", backupData.KeyAssociations.Count);
}
// Export Profile Schedules
if (options.IncludeProfileSchedules)
{
_logger.LogDebug("Esportazione schedule profili in corso...");
backupData.ProfileSchedules = await ExportProfileSchedulesAsync(options.IncludeOnlyActiveRecords);
result.ProcessedCounts.ProfileSchedules = backupData.ProfileSchedules.Count;
_logger.LogDebug("Esportate {Count} schedule", backupData.ProfileSchedules.Count);
}
// Aggiorna metadata con conteggi
backupData.Metadata.RecordCounts = result.ProcessedCounts;
// Serializza e salva
var jsonOptions = new JsonSerializerOptions
{
WriteIndented = true,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var json = JsonSerializer.Serialize(backupData, jsonOptions);
var fileName = $"data_coupler_backup_{DateTime.Now:yyyyMMdd_HHmmss}.json";
var documentsPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
var backupFolder = Path.Combine(documentsPath, "DataCoupler", "Backups");
Directory.CreateDirectory(backupFolder);
var filePath = Path.Combine(backupFolder, fileName);
await File.WriteAllTextAsync(filePath, json);
result.FilePath = filePath;
result.Success = true;
result.Message = $"Backup completato con successo. File salvato: {filePath}";
_logger.LogInformation("Backup completato: {FilePath}, Record totali: {Total}",
filePath,
result.ProcessedCounts.Profiles + result.ProcessedCounts.Credentials +
result.ProcessedCounts.KeyAssociations + result.ProcessedCounts.ProfileSchedules);
}
catch (Exception ex)
{
result.Success = false;
result.Message = $"Errore durante il backup: {ex.Message}";
result.Errors.Add(ex.ToString());
_logger.LogError(ex, "Errore durante l'esportazione del backup");
}
finally
{
stopwatch.Stop();
result.Duration = stopwatch.Elapsed;
}
return result;
}
/// <summary>
/// Importa dati da file backup
/// </summary>
public async Task<BackupOperationResult> ImportBackupAsync(string backupFilePath, RestoreOptions options)
{
if (!File.Exists(backupFilePath))
{
return new BackupOperationResult
{
Success = false,
Message = "File di backup non trovato"
};
}
var json = await File.ReadAllTextAsync(backupFilePath);
return await ImportBackupFromJsonAsync(json, options);
}
/// <summary>
/// Importa dati da contenuto JSON
/// </summary>
public async Task<BackupOperationResult> ImportBackupFromJsonAsync(string jsonContent, RestoreOptions options)
{
var result = new BackupOperationResult();
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
try
{
_logger.LogInformation("Avvio import backup con opzioni: {@Options}", options);
// Parse JSON
var jsonOptions = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var backupData = JsonSerializer.Deserialize<SystemBackupData>(jsonContent, jsonOptions);
if (backupData == null)
{
throw new InvalidOperationException("Impossibile deserializzare i dati del backup");
}
_logger.LogInformation("Backup da importare: {Metadata}", backupData.Metadata);
// Crea backup automatico prima del restore se richiesto
if (options.CreateBackupBeforeRestore)
{
_logger.LogInformation("Creazione backup di sicurezza pre-restore...");
var preRestoreBackup = await ExportBackupAsync(new BackupOptions
{
Description = "Backup automatico pre-restore",
CreatedBy = options.ImportedBy
});
if (preRestoreBackup.Success)
{
result.Warnings.Add($"Backup di sicurezza creato: {preRestoreBackup.FilePath}");
}
}
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Import Profiles
if (options.RestoreProfiles && backupData.Profiles.Any())
{
_logger.LogDebug("Importazione profili in corso...");
var importedProfiles = await ImportProfilesAsync(backupData.Profiles, options);
result.ProcessedCounts.Profiles = importedProfiles;
_logger.LogDebug("Importati {Count} profili", importedProfiles);
}
// Import Credentials (solo metadati, nessun dato sensibile)
if (options.RestoreCredentials && backupData.Credentials.Any())
{
_logger.LogDebug("Importazione credenziali in corso...");
var importedCredentials = await ImportCredentialsAsync(backupData.Credentials, options);
result.ProcessedCounts.Credentials = importedCredentials;
result.Warnings.Add("Le credenziali importate richiedono configurazione manuale di password/API keys");
_logger.LogDebug("Importate {Count} credenziali", importedCredentials);
}
// Import Key Associations
if (options.RestoreKeyAssociations && backupData.KeyAssociations.Any())
{
_logger.LogDebug("Importazione associazioni chiavi in corso...");
var importedAssociations = await ImportKeyAssociationsAsync(backupData.KeyAssociations, options);
result.ProcessedCounts.KeyAssociations = importedAssociations;
_logger.LogDebug("Importate {Count} associazioni", importedAssociations);
}
// Import Profile Schedules
if (options.RestoreProfileSchedules && backupData.ProfileSchedules.Any())
{
_logger.LogDebug("Importazione schedule profili in corso...");
var importedSchedules = await ImportProfileSchedulesAsync(backupData.ProfileSchedules, options);
result.ProcessedCounts.ProfileSchedules = importedSchedules;
_logger.LogDebug("Importate {Count} schedule", importedSchedules);
}
await transaction.CommitAsync();
result.Success = true;
result.Message = "Import completato con successo";
_logger.LogInformation("Import completato con successo. Record totali: {Total}",
result.ProcessedCounts.Profiles + result.ProcessedCounts.Credentials +
result.ProcessedCounts.KeyAssociations + result.ProcessedCounts.ProfileSchedules);
}
catch
{
await transaction.RollbackAsync();
throw;
}
}
catch (Exception ex)
{
result.Success = false;
result.Message = $"Errore durante l'import: {ex.Message}";
result.Errors.Add(ex.ToString());
_logger.LogError(ex, "Errore durante l'importazione del backup");
}
finally
{
stopwatch.Stop();
result.Duration = stopwatch.Elapsed;
}
return result;
}
/// <summary>
/// Valida un file di backup
/// </summary>
public async Task<BackupOperationResult> ValidateBackupAsync(string backupFilePath)
{
var result = new BackupOperationResult();
try
{
if (!File.Exists(backupFilePath))
{
result.Errors.Add("File non trovato");
return result;
}
var json = await File.ReadAllTextAsync(backupFilePath);
var backupData = JsonSerializer.Deserialize<SystemBackupData>(json);
if (backupData == null)
{
result.Errors.Add("Formato backup non valido");
return result;
}
// Validazioni
if (backupData.Metadata == null)
{
result.Errors.Add("Metadata mancanti");
}
if (string.IsNullOrEmpty(backupData.Metadata?.Version))
{
result.Warnings.Add("Versione backup non specificata");
}
result.ProcessedCounts.Profiles = backupData.Profiles?.Count ?? 0;
result.ProcessedCounts.Credentials = backupData.Credentials?.Count ?? 0;
result.ProcessedCounts.KeyAssociations = backupData.KeyAssociations?.Count ?? 0;
result.ProcessedCounts.ProfileSchedules = backupData.ProfileSchedules?.Count ?? 0;
result.Success = result.Errors.Count == 0;
result.Message = result.Success ? "Backup valido" : "Backup contiene errori";
}
catch (Exception ex)
{
result.Success = false;
result.Message = $"Errore validazione: {ex.Message}";
result.Errors.Add(ex.ToString());
}
return result;
}
/// <summary>
/// Ottiene informazioni backup senza importare
/// </summary>
public async Task<SystemBackupData?> GetBackupInfoAsync(string backupFilePath)
{
try
{
if (!File.Exists(backupFilePath))
return null;
var json = await File.ReadAllTextAsync(backupFilePath);
return JsonSerializer.Deserialize<SystemBackupData>(json);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Errore lettura info backup da {FilePath}", backupFilePath);
return null;
}
}
#region Private Export Methods
private async Task<List<DataCouplerProfileBackup>> ExportProfilesAsync(bool onlyActive)
{
var query = _context.DataCouplerProfiles
.Include(p => p.SourceCredential)
.Include(p => p.DestinationCredential)
.AsQueryable();
if (onlyActive)
query = query.Where(p => p.IsActive);
var profiles = await query.ToListAsync();
return profiles.Select(p => new DataCouplerProfileBackup
{
Id = p.Id,
Name = p.Name,
Description = p.Description,
SourceType = p.SourceType,
SourceCredentialName = p.SourceCredential?.Name,
SourceDatabaseName = p.SourceDatabaseName,
SourceSchema = p.SourceSchema,
SourceTable = p.SourceTable,
SourceCustomQuery = p.SourceCustomQuery,
SourceFilePath = p.SourceFilePath,
DestinationType = p.DestinationType,
DestinationCredentialName = p.DestinationCredential?.Name,
DestinationSchema = p.DestinationSchema,
DestinationTable = p.DestinationTable,
DestinationEndpoint = p.DestinationEndpoint,
FieldMappings = !string.IsNullOrEmpty(p.FieldMappingJson) ?
System.Text.Json.JsonSerializer.Deserialize<List<FieldMappingDto>>(p.FieldMappingJson) ?? new List<FieldMappingDto>() :
new List<FieldMappingDto>(),
SourceKeyField = p.SourceKeyField,
UseRecordAssociations = p.UseRecordAssociations,
CreatedBy = p.CreatedBy,
CreatedAt = p.CreatedAt,
LastUsedAt = p.LastUsedAt,
IsActive = p.IsActive
}).ToList();
}
private async Task<List<CredentialBackup>> ExportCredentialsAsync(bool onlyActive)
{
var query = _context.Credentials.AsQueryable();
if (onlyActive)
query = query.Where(c => c.IsActive);
var credentials = await query.ToListAsync();
return credentials.Select(c => new CredentialBackup
{
Id = c.Id,
Name = c.Name,
Type = c.Type,
DatabaseType = c.DatabaseType,
Host = c.Host,
Port = c.Port,
DatabaseName = c.DatabaseName,
Username = c.Username,
// Password, API Keys e Token NON inclusi per sicurezza
CommandTimeout = c.CommandTimeout,
TimeoutSeconds = c.TimeoutSeconds,
IgnoreSslErrors = c.IgnoreSslErrors,
RestServiceType = c.RestServiceType,
Headers = c.Headers,
AdditionalParameters = c.AdditionalParameters,
CreatedAt = c.CreatedAt,
UpdatedAt = c.UpdatedAt,
CreatedBy = c.CreatedBy,
IsActive = c.IsActive
}).ToList();
}
private async Task<List<KeyAssociationBackup>> ExportKeyAssociationsAsync(bool onlyActive)
{
var query = _context.KeyAssociations.AsQueryable();
if (onlyActive)
query = query.Where(ka => ka.IsActive);
var associations = await query.ToListAsync();
return associations.Select(ka => new KeyAssociationBackup
{
Id = ka.Id,
KeyValue = ka.KeyValue,
SourceKeyField = ka.SourceKeyField,
DestinationKeyField = ka.DestinationKeyField,
DestinationEntity = ka.DestinationEntity,
DestinationId = ka.DestinationId,
RestCredentialName = ka.RestCredentialName,
CreatedAt = ka.CreatedAt,
UpdatedAt = ka.UpdatedAt,
LastVerifiedAt = ka.LastVerifiedAt,
IsActive = ka.IsActive,
DataHash = ka.Data_Hash,
SourcesInfo = ka.SourcesInfo,
AdditionalInfo = ka.AdditionalInfo
}).ToList();
}
private Task<List<ProfileScheduleBackup>> ExportProfileSchedulesAsync(bool onlyActive)
{
// Nota: Assumendo che esista una tabella ProfileSchedules
// Se non esiste, questo metodo restituirà una lista vuota
var schedules = new List<ProfileScheduleBackup>();
try
{
// TODO: Implementare quando la tabella ProfileSchedules sarà disponibile
// var query = _context.ProfileSchedules.AsQueryable();
// if (onlyActive)
// query = query.Where(ps => ps.IsActive);
// var profileSchedules = await query.ToListAsync();
// schedules = profileSchedules.Select(ps => new ProfileScheduleBackup { ... }).ToList();
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Tabella ProfileSchedules non disponibile, saltando export");
}
return Task.FromResult(schedules);
}
#endregion
#region Private Import Methods
private async Task<int> ImportProfilesAsync(List<DataCouplerProfileBackup> profiles, RestoreOptions options)
{
int imported = 0;
foreach (var profileBackup in profiles)
{
try
{
var existing = await _context.DataCouplerProfiles
.FirstOrDefaultAsync(p => p.Name == profileBackup.Name);
if (existing != null && !options.OverwriteExisting)
{
_logger.LogDebug("Profilo {Name} già esistente, saltando", profileBackup.Name);
continue;
}
var profile = new DataCouplerProfile
{
Name = profileBackup.Name,
Description = profileBackup.Description,
SourceType = profileBackup.SourceType,
SourceDatabaseName = profileBackup.SourceDatabaseName,
SourceSchema = profileBackup.SourceSchema,
SourceTable = profileBackup.SourceTable,
SourceCustomQuery = profileBackup.SourceCustomQuery,
SourceFilePath = profileBackup.SourceFilePath,
DestinationType = profileBackup.DestinationType,
DestinationSchema = profileBackup.DestinationSchema,
DestinationTable = profileBackup.DestinationTable,
DestinationEndpoint = profileBackup.DestinationEndpoint,
FieldMappingJson = profileBackup.FieldMappings != null ?
System.Text.Json.JsonSerializer.Serialize(profileBackup.FieldMappings) :
string.Empty,
SourceKeyField = profileBackup.SourceKeyField,
UseRecordAssociations = profileBackup.UseRecordAssociations,
CreatedBy = options.ImportedBy ?? profileBackup.CreatedBy,
CreatedAt = DateTime.UtcNow,
IsActive = profileBackup.IsActive
};
// Risolvi credential IDs per nome se esistenti
if (!string.IsNullOrEmpty(profileBackup.SourceCredentialName))
{
var sourceCred = await _context.Credentials
.FirstOrDefaultAsync(c => c.Name == profileBackup.SourceCredentialName);
profile.SourceCredentialId = sourceCred?.Id;
}
if (!string.IsNullOrEmpty(profileBackup.DestinationCredentialName))
{
var destCred = await _context.Credentials
.FirstOrDefaultAsync(c => c.Name == profileBackup.DestinationCredentialName);
profile.DestinationCredentialId = destCred?.Id;
}
if (existing != null)
{
// Update existing
_context.Entry(existing).CurrentValues.SetValues(profile);
existing.Id = profileBackup.Id; // Mantieni ID originale se sovrascriviamo
}
else
{
// Add new
_context.DataCouplerProfiles.Add(profile);
}
imported++;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Errore importazione profilo {Name}", profileBackup.Name);
}
}
await _context.SaveChangesAsync();
return imported;
}
private async Task<int> ImportCredentialsAsync(List<CredentialBackup> credentials, RestoreOptions options)
{
int imported = 0;
foreach (var credBackup in credentials)
{
try
{
var existing = await _context.Credentials
.FirstOrDefaultAsync(c => c.Name == credBackup.Name);
if (existing != null && !options.OverwriteExisting)
{
_logger.LogDebug("Credenziale {Name} già esistente, saltando", credBackup.Name);
continue;
}
var credential = new CredentialEntity
{
Name = credBackup.Name,
Type = credBackup.Type,
DatabaseType = credBackup.DatabaseType,
Host = credBackup.Host,
Port = credBackup.Port,
DatabaseName = credBackup.DatabaseName,
Username = credBackup.Username,
// Password, API Keys e Token dovranno essere riconfigurati manualmente
CommandTimeout = credBackup.CommandTimeout,
TimeoutSeconds = credBackup.TimeoutSeconds,
IgnoreSslErrors = credBackup.IgnoreSslErrors,
RestServiceType = credBackup.RestServiceType,
Headers = credBackup.Headers,
AdditionalParameters = credBackup.AdditionalParameters,
CreatedAt = DateTime.UtcNow,
CreatedBy = options.ImportedBy ?? credBackup.CreatedBy,
IsActive = credBackup.IsActive
};
if (existing != null)
{
// Update existing (preserva password esistenti)
var oldPassword = existing.EncryptedPassword;
var oldApiKey = existing.EncryptedApiKey;
var oldAuthToken = existing.EncryptedAuthToken;
_context.Entry(existing).CurrentValues.SetValues(credential);
existing.Id = credBackup.Id;
existing.EncryptedPassword = oldPassword;
existing.EncryptedApiKey = oldApiKey;
existing.EncryptedAuthToken = oldAuthToken;
}
else
{
// Add new
_context.Credentials.Add(credential);
}
imported++;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Errore importazione credenziale {Name}", credBackup.Name);
}
}
await _context.SaveChangesAsync();
return imported;
}
private async Task<int> ImportKeyAssociationsAsync(List<KeyAssociationBackup> associations, RestoreOptions options)
{
int imported = 0;
foreach (var assocBackup in associations)
{
try
{
var existing = await _context.KeyAssociations
.FirstOrDefaultAsync(ka => ka.KeyValue == assocBackup.KeyValue &&
ka.DestinationEntity == assocBackup.DestinationEntity &&
ka.RestCredentialName == assocBackup.RestCredentialName);
if (existing != null && !options.OverwriteExisting)
{
_logger.LogDebug("Associazione {Key}-{Entity} già esistente, saltando",
assocBackup.KeyValue, assocBackup.DestinationEntity);
continue;
}
var association = new KeyAssociation
{
KeyValue = assocBackup.KeyValue,
SourceKeyField = assocBackup.SourceKeyField,
DestinationKeyField = assocBackup.DestinationKeyField,
DestinationEntity = assocBackup.DestinationEntity,
DestinationId = assocBackup.DestinationId,
RestCredentialName = assocBackup.RestCredentialName,
CreatedAt = DateTime.UtcNow,
IsActive = assocBackup.IsActive,
Data_Hash = assocBackup.DataHash,
SourcesInfo = assocBackup.SourcesInfo,
AdditionalInfo = assocBackup.AdditionalInfo
};
if (existing != null)
{
// Update existing
_context.Entry(existing).CurrentValues.SetValues(association);
existing.Id = assocBackup.Id;
}
else
{
// Add new
_context.KeyAssociations.Add(association);
}
imported++;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Errore importazione associazione {Key}-{Entity}",
assocBackup.KeyValue, assocBackup.DestinationEntity);
}
}
await _context.SaveChangesAsync();
return imported;
}
private Task<int> ImportProfileSchedulesAsync(List<ProfileScheduleBackup> schedules, RestoreOptions options)
{
int imported = 0;
try
{
// TODO: Implementare quando la tabella ProfileSchedules sarà disponibile
_logger.LogInformation("Import ProfileSchedules saltato - tabella non ancora implementata");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Errore importazione schedule profili");
}
return Task.FromResult(imported);
}
#endregion
}
@@ -0,0 +1,210 @@
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);
}
}
+195
View File
@@ -0,0 +1,195 @@
using System.Globalization;
namespace Data_Coupler.Services;
/// <summary>
/// Servizio utility per la gestione di date, orari e formattazione
/// Utilizza il formato 24h per coerenza con il sistema
/// </summary>
public static class DateTimeHelper
{
/// <summary>
/// Formato orario 24h standard utilizzato in tutto il sistema
/// </summary>
public const string TimeFormat24H = "HH:mm";
/// <summary>
/// Formato data/ora 24h completo utilizzato per il logging e la visualizzazione
/// </summary>
public const string DateTimeFormat24H = "dd/MM/yyyy HH:mm:ss";
/// <summary>
/// Formato data/ora 24h per i log dettagliati
/// </summary>
public const string DetailedDateTimeFormat24H = "dd/MM/yyyy HH:mm:ss.fff";
/// <summary>
/// Cultura italiana per la formattazione (formato 24h di default)
/// </summary>
public static readonly CultureInfo ItalianCulture = new("it-IT");
/// <summary>
/// Converte un TimeSpan in stringa formato 24h (HH:mm)
/// </summary>
public static string FormatTime24H(TimeSpan time)
{
return time.ToString(TimeFormat24H);
}
/// <summary>
/// Converte un DateTime in stringa formato 24h (dd/MM/yyyy HH:mm:ss)
/// </summary>
public static string FormatDateTime24H(DateTime dateTime)
{
return dateTime.ToString(DateTimeFormat24H, ItalianCulture);
}
/// <summary>
/// Converte un DateTime in stringa formato 24h dettagliato con millisecondi
/// </summary>
public static string FormatDateTimeDetailed24H(DateTime dateTime)
{
return dateTime.ToString(DetailedDateTimeFormat24H, ItalianCulture);
}
/// <summary>
/// Prova a parsare una stringa orario in formato 24h
/// </summary>
public static bool TryParseTime24H(string? timeString, out TimeSpan time)
{
time = default;
if (string.IsNullOrWhiteSpace(timeString))
{
return false;
}
return TimeSpan.TryParseExact(timeString.Trim(), TimeFormat24H, ItalianCulture, out time);
}
/// <summary>
/// Prova a parsare una stringa data/ora in formato 24h
/// </summary>
public static bool TryParseDateTime24H(string? dateTimeString, out DateTime dateTime)
{
dateTime = default;
if (string.IsNullOrWhiteSpace(dateTimeString))
{
return false;
}
return DateTime.TryParseExact(dateTimeString.Trim(), DateTimeFormat24H, ItalianCulture, DateTimeStyles.None, out dateTime);
}
/// <summary>
/// Ottiene l'ora corrente locale formattata in 24h
/// </summary>
public static string GetCurrentTime24H()
{
return FormatTime24H(DateTime.Now.TimeOfDay);
}
/// <summary>
/// Ottiene la data/ora corrente locale formattata in 24h
/// </summary>
public static string GetCurrentDateTime24H()
{
return FormatDateTime24H(DateTime.Now);
}
/// <summary>
/// Valida se una stringa rappresenta un orario valido nel formato 24h
/// </summary>
public static bool IsValidTime24H(string? timeString)
{
return TryParseTime24H(timeString, out _);
}
/// <summary>
/// Valida se una stringa rappresenta una data/ora valida nel formato 24h
/// </summary>
public static bool IsValidDateTime24H(string? dateTimeString)
{
return TryParseDateTime24H(dateTimeString, out _);
}
/// <summary>
/// Converte un orario dal formato 12h al formato 24h se necessario
/// </summary>
public static string? ConvertTo24H(string? timeString)
{
if (string.IsNullOrWhiteSpace(timeString))
{
return timeString;
}
// Se è già in formato 24h, restituisci così com'è
if (TryParseTime24H(timeString, out var time24))
{
return FormatTime24H(time24);
}
// Prova a parsare dal formato 12h
if (DateTime.TryParse(timeString.Trim(), ItalianCulture, DateTimeStyles.None, out var parsed))
{
return FormatTime24H(parsed.TimeOfDay);
}
return null; // Formato non riconosciuto
}
/// <summary>
/// Calcola il tempo rimanente fino al prossimo orario schedulato
/// </summary>
public static TimeSpan TimeUntilNextSchedule(TimeSpan scheduledTime, DateTime? referenceTime = null)
{
var now = referenceTime ?? DateTime.Now;
var currentTime = now.TimeOfDay;
// Se l'orario programmato è già passato oggi, programma per domani
if (currentTime > scheduledTime)
{
var tomorrow = now.Date.AddDays(1);
var nextExecution = tomorrow.Add(scheduledTime);
return nextExecution - now;
}
else
{
var today = now.Date;
var nextExecution = today.Add(scheduledTime);
return nextExecution - now;
}
}
/// <summary>
/// Ottiene il nome del giorno della settimana in italiano
/// </summary>
public static string GetDayOfWeekName(DayOfWeek dayOfWeek)
{
return dayOfWeek switch
{
DayOfWeek.Sunday => "Domenica",
DayOfWeek.Monday => "Lunedì",
DayOfWeek.Tuesday => "Martedì",
DayOfWeek.Wednesday => "Mercoledì",
DayOfWeek.Thursday => "Giovedì",
DayOfWeek.Friday => "Venerdì",
DayOfWeek.Saturday => "Sabato",
_ => dayOfWeek.ToString()
};
}
/// <summary>
/// Ottiene il nome del giorno della settimana in italiano tramite indice (0=Domenica)
/// </summary>
public static string GetDayOfWeekName(int dayOfWeekIndex)
{
if (dayOfWeekIndex < 0 || dayOfWeekIndex > 6)
{
return "Non valido";
}
return GetDayOfWeekName((DayOfWeek)dayOfWeekIndex);
}
}
@@ -0,0 +1,24 @@
namespace Data_Coupler.Services;
/// <summary>
/// Risultato dell'esecuzione di un profilo schedulato
/// </summary>
public class ProfileExecutionResult
{
public bool Success { get; set; }
public string Message { get; set; } = string.Empty;
public int RecordsProcessed { get; set; }
public DateTime ExecutionTime { get; set; }
public TimeSpan Duration { get; set; }
}
/// <summary>
/// Interfaccia per l'esecuzione di profili schedulati
/// </summary>
public interface IScheduledProfileExecutionService
{
/// <summary>
/// Esegue un profilo Data Coupler specificato dall'ID
/// </summary>
Task<ProfileExecutionResult> ExecuteProfileAsync(int profileId);
}
@@ -0,0 +1,388 @@
using CredentialManager.Models;
using CredentialManager.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Data_Coupler.Services;
/// <summary>
/// Servizio background che gestisce l'esecuzione automatica dei profili schedulati
/// Controlla ogni minuto se ci sono profili da eseguire secondo la schedulazione impostata
/// </summary>
public class ScheduledExecutionBackgroundService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<ScheduledExecutionBackgroundService> _logger;
private readonly TimeSpan _checkInterval = TimeSpan.FromMinutes(1); // Controlla ogni minuto
public ScheduledExecutionBackgroundService(
IServiceProvider serviceProvider,
ILogger<ScheduledExecutionBackgroundService> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Servizio di schedulazione automatica avviato. Controllo ogni {CheckInterval} minuti.",
_checkInterval.TotalMinutes);
while (!stoppingToken.IsCancellationRequested)
{
try
{
await CheckAndExecuteScheduledProfiles();
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore durante il controllo delle schedulazioni");
}
// Attendi il prossimo controllo
await Task.Delay(_checkInterval, stoppingToken);
}
_logger.LogInformation("Servizio di schedulazione automatica arrestato.");
}
private async Task CheckAndExecuteScheduledProfiles()
{
using var scope = _serviceProvider.CreateScope();
var scheduleService = scope.ServiceProvider.GetRequiredService<IProfileScheduleService>();
var executionService = scope.ServiceProvider.GetRequiredService<IScheduledProfileExecutionService>();
var profileService = scope.ServiceProvider.GetRequiredService<IDataCouplerProfileService>();
try
{
// Ottieni tutte le schedulazioni attive
var activeSchedules = await scheduleService.GetActiveSchedulesAsync();
var currentTime = DateTime.Now; // Usa l'ora locale per il confronto con le schedulazioni
_logger.LogDebug("Controllo schedulazioni: {ScheduleCount} schedulazioni attive alle {CurrentTime}",
activeSchedules.Count, DateTimeHelper.FormatDateTime24H(currentTime));
foreach (var schedule in activeSchedules)
{
try
{
if (ShouldExecuteSchedule(schedule, currentTime))
{
_logger.LogInformation("Esecuzione schedulata per profilo: {ProfileName} (Schedule: {ScheduleName})",
schedule.Profile?.Name ?? "N/A", schedule.Name);
// Esegui il profilo
var result = await executionService.ExecuteProfileAsync(schedule.ProfileId);
// Aggiorna la schedulazione
await UpdateScheduleAfterExecution(scheduleService, schedule, currentTime, result.Success);
// Log del risultato
if (result.Success)
{
_logger.LogInformation("Esecuzione schedulata completata con successo per {ProfileName}. " +
"Record processati: {RecordsProcessed}, Durata: {Duration}ms",
schedule.Profile?.Name ?? "N/A", result.RecordsProcessed, result.Duration.TotalMilliseconds);
}
else
{
_logger.LogError("Esecuzione schedulata fallita per {ProfileName}: {ErrorMessage}",
schedule.Profile?.Name ?? "N/A", result.Message);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore durante l'esecuzione della schedulazione {ScheduleName} per il profilo {ProfileName}",
schedule.Name, schedule.Profile?.Name ?? "N/A");
// Aggiorna la schedulazione anche in caso di errore
try
{
await UpdateScheduleAfterExecution(scheduleService, schedule, currentTime, false);
}
catch (Exception updateEx)
{
_logger.LogError(updateEx, "Errore durante l'aggiornamento della schedulazione {ScheduleName} dopo l'errore",
schedule.Name);
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore durante il recupero delle schedulazioni attive");
}
}
/// <summary>
/// Determina se una schedulazione deve essere eseguita in base all'orario corrente
/// </summary>
private bool ShouldExecuteSchedule(ProfileSchedule schedule, DateTime currentTime)
{
if (!schedule.IsEnabled)
{
return false;
}
// Controllo per evitare esecuzioni multiple nella stessa finestra temporale (tolleranza di 1 minuto)
if (schedule.LastExecutionTime.HasValue)
{
var timeSinceLastExecution = currentTime - schedule.LastExecutionTime.Value;
if (timeSinceLastExecution.TotalMinutes < 1)
{
return false;
}
}
return schedule.ScheduleType.ToLower() switch
{
"once" => ShouldExecuteOnceSchedule(schedule, currentTime),
"daily" => ShouldExecuteDailySchedule(schedule, currentTime),
"weekly" => ShouldExecuteWeeklySchedule(schedule, currentTime),
"monthly" => ShouldExecuteMonthlySchedule(schedule, currentTime),
_ => false
};
}
private bool ShouldExecuteOnceSchedule(ProfileSchedule schedule, DateTime currentTime)
{
if (!schedule.ScheduledDateTime.HasValue)
{
_logger.LogWarning("Schedulazione 'once' senza data/ora specificata: {ScheduleName}", schedule.Name);
return false;
}
var scheduledTime = schedule.ScheduledDateTime.Value;
// Esegui se l'orario programmato è passato e non è mai stato eseguito
return currentTime >= scheduledTime && !schedule.LastExecutionTime.HasValue;
}
private bool ShouldExecuteDailySchedule(ProfileSchedule schedule, DateTime currentTime)
{
if (string.IsNullOrEmpty(schedule.DailyTime))
{
_logger.LogWarning("Schedulazione 'daily' senza orario specificato: {ScheduleName}", schedule.Name);
return false;
}
if (!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
{
_logger.LogWarning("Formato orario non valido per schedulazione daily {ScheduleName}: {DailyTime} (formato richiesto: HH:mm)",
schedule.Name, schedule.DailyTime);
return false;
}
var currentTimeOfDay = currentTime.TimeOfDay;
var scheduledTimeOfDay = scheduledTime;
// Verifica se siamo nell'orario giusto (con tolleranza di ±1 minuto)
var timeDifference = Math.Abs((currentTimeOfDay - scheduledTimeOfDay).TotalMinutes);
if (timeDifference > 1)
{
return false;
}
// Se non è mai stato eseguito, esegui
if (!schedule.LastExecutionTime.HasValue)
{
return true;
}
// Se è stato eseguito, verifica che sia passato almeno un giorno
var lastExecutionDate = schedule.LastExecutionTime.Value.Date;
var currentDate = currentTime.Date;
return currentDate > lastExecutionDate;
}
private bool ShouldExecuteWeeklySchedule(ProfileSchedule schedule, DateTime currentTime)
{
if (!schedule.DayOfWeek.HasValue || string.IsNullOrEmpty(schedule.DailyTime))
{
_logger.LogWarning("Schedulazione 'weekly' senza giorno della settimana o orario specificato: {ScheduleName}",
schedule.Name);
return false;
}
if (!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
{
_logger.LogWarning("Formato orario non valido per schedulazione weekly {ScheduleName}: {DailyTime} (formato richiesto: HH:mm)",
schedule.Name, schedule.DailyTime);
return false;
}
// Verifica il giorno della settimana (0=Domenica, 1=Lunedì, etc.)
var currentDayOfWeek = (int)currentTime.DayOfWeek;
if (currentDayOfWeek != schedule.DayOfWeek.Value)
{
return false;
}
// Verifica l'orario (con tolleranza di ±1 minuto)
var currentTimeOfDay = currentTime.TimeOfDay;
var timeDifference = Math.Abs((currentTimeOfDay - scheduledTime).TotalMinutes);
if (timeDifference > 1)
{
return false;
}
// Se non è mai stato eseguito, esegui
if (!schedule.LastExecutionTime.HasValue)
{
return true;
}
// Se è stato eseguito, verifica che sia passata almeno una settimana
var daysSinceLastExecution = (currentTime.Date - schedule.LastExecutionTime.Value.Date).TotalDays;
return daysSinceLastExecution >= 7;
}
private bool ShouldExecuteMonthlySchedule(ProfileSchedule schedule, DateTime currentTime)
{
if (!schedule.DayOfMonth.HasValue || string.IsNullOrEmpty(schedule.DailyTime))
{
_logger.LogWarning("Schedulazione 'monthly' senza giorno del mese o orario specificato: {ScheduleName}",
schedule.Name);
return false;
}
if (!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
{
_logger.LogWarning("Formato orario non valido per schedulazione monthly {ScheduleName}: {DailyTime} (formato richiesto: HH:mm)",
schedule.Name, schedule.DailyTime);
return false;
}
// Verifica il giorno del mese
var currentDayOfMonth = currentTime.Day;
var scheduledDayOfMonth = schedule.DayOfMonth.Value;
// Gestione per mesi con meno giorni (es. 31 in febbraio diventa ultimo giorno del mese)
var daysInCurrentMonth = DateTime.DaysInMonth(currentTime.Year, currentTime.Month);
var effectiveScheduledDay = Math.Min(scheduledDayOfMonth, daysInCurrentMonth);
if (currentDayOfMonth != effectiveScheduledDay)
{
return false;
}
// Verifica l'orario (con tolleranza di ±1 minuto)
var currentTimeOfDay = currentTime.TimeOfDay;
var timeDifference = Math.Abs((currentTimeOfDay - scheduledTime).TotalMinutes);
if (timeDifference > 1)
{
return false;
}
// Se non è mai stato eseguito, esegui
if (!schedule.LastExecutionTime.HasValue)
{
return true;
}
// Se è stato eseguito, verifica che sia passato almeno un mese
var lastExecutionDate = schedule.LastExecutionTime.Value;
var monthsSinceLastExecution = ((currentTime.Year - lastExecutionDate.Year) * 12) + currentTime.Month - lastExecutionDate.Month;
return monthsSinceLastExecution >= 1;
}
/// <summary>
/// Aggiorna la schedulazione dopo l'esecuzione
/// </summary>
private async Task UpdateScheduleAfterExecution(IProfileScheduleService scheduleService,
ProfileSchedule schedule, DateTime executionTime, bool wasSuccessful)
{
try
{
// Aggiorna i dati della schedulazione
schedule.LastExecutionTime = executionTime;
schedule.ExecutionCount++;
// Calcola il prossimo tempo di esecuzione
schedule.NextExecutionTime = CalculateNextExecutionTime(schedule, executionTime);
// Per schedulazioni "once", disabilita dopo l'esecuzione
if (schedule.ScheduleType.ToLower() == "once")
{
schedule.IsEnabled = false;
_logger.LogInformation("Schedulazione 'once' {ScheduleName} disabilitata dopo l'esecuzione", schedule.Name);
}
await scheduleService.UpdateScheduleAsync(schedule);
_logger.LogDebug("Schedulazione {ScheduleName} aggiornata. Prossima esecuzione: {NextExecution}",
schedule.Name, schedule.NextExecutionTime.HasValue ? DateTimeHelper.FormatDateTime24H(schedule.NextExecutionTime.Value) : "N/A");
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore durante l'aggiornamento della schedulazione {ScheduleName}", schedule.Name);
}
}
/// <summary>
/// Calcola il prossimo tempo di esecuzione per una schedulazione
/// </summary>
private DateTime? CalculateNextExecutionTime(ProfileSchedule schedule, DateTime lastExecution)
{
if (!schedule.IsEnabled)
{
return null;
}
return schedule.ScheduleType.ToLower() switch
{
"once" => null, // Schedulazione singola non ha prossima esecuzione
"daily" => CalculateNextDailyExecution(schedule, lastExecution),
"weekly" => CalculateNextWeeklyExecution(schedule, lastExecution),
"monthly" => CalculateNextMonthlyExecution(schedule, lastExecution),
_ => null
};
}
private DateTime? CalculateNextDailyExecution(ProfileSchedule schedule, DateTime lastExecution)
{
if (string.IsNullOrEmpty(schedule.DailyTime) || !DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
{
return null;
}
var nextDate = lastExecution.Date.AddDays(1);
return nextDate.Add(scheduledTime);
}
private DateTime? CalculateNextWeeklyExecution(ProfileSchedule schedule, DateTime lastExecution)
{
if (!schedule.DayOfWeek.HasValue || string.IsNullOrEmpty(schedule.DailyTime) ||
!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
{
return null;
}
var nextDate = lastExecution.Date.AddDays(7);
return nextDate.Add(scheduledTime);
}
private DateTime? CalculateNextMonthlyExecution(ProfileSchedule schedule, DateTime lastExecution)
{
if (!schedule.DayOfMonth.HasValue || string.IsNullOrEmpty(schedule.DailyTime) ||
!DateTimeHelper.TryParseTime24H(schedule.DailyTime, out var scheduledTime))
{
return null;
}
var nextMonth = lastExecution.AddMonths(1);
var daysInNextMonth = DateTime.DaysInMonth(nextMonth.Year, nextMonth.Month);
var effectiveDay = Math.Min(schedule.DayOfMonth.Value, daysInNextMonth);
var nextDate = new DateTime(nextMonth.Year, nextMonth.Month, effectiveDay);
return nextDate.Add(scheduledTime);
}
}
File diff suppressed because it is too large Load Diff