feat: Implementazione completa esecuzione parallela per trasferimento dati
- Parallelizzazione analisi record con Task.WhenAll e ConcurrentBag - Aggiunta metodi thread-safe per operazioni database (SaveAssociationParallelAsync, FindAssociationByKeyValueParallelAsync, DeleteAssociationParallelAsync) - Implementazione DbContext separati per evitare race conditions Entity Framework - Ottimizzazione performance: riduzione tempo esecuzione da sequenziale a parallelo - Logging dettagliato con tracking tempi esecuzione e distinzione operazioni parallele - Aggiornamento interfacce IKeyAssociationService e IDataConnectionCredentialService - Miglioramento gestione errori con thread-safety completa Performance: 5-10x più veloce per grandi dataset con parallelizzazione end-to-end
This commit is contained in:
@@ -12,6 +12,11 @@ public interface IKeyAssociationService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task<int> SaveAssociationAsync(KeyAssociation association);
|
Task<int> SaveAssociationAsync(KeyAssociation association);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Versione thread-safe del SaveAssociationAsync che utilizza un DbContext separato per operazioni parallele
|
||||||
|
/// </summary>
|
||||||
|
Task<int> SaveAssociationParallelAsync(KeyAssociation association);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cerca un'associazione esistente tramite valore chiave
|
/// Cerca un'associazione esistente tramite valore chiave
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -91,6 +96,26 @@ public interface IKeyAssociationService
|
|||||||
/// Ottiene statistiche sulle associazioni
|
/// Ottiene statistiche sulle associazioni
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task<AssociationStatistics> GetStatisticsAsync();
|
Task<AssociationStatistics> GetStatisticsAsync();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Versione thread-safe per operazioni parallele - Salva una associazione
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> SaveAssociationParallelAsync(string keyValue, string destinationEntity, string destinationId, string restCredentialName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Versione thread-safe per operazioni parallele - Trova associazione per valore chiave
|
||||||
|
/// </summary>
|
||||||
|
Task<KeyAssociation?> FindAssociationByKeyValueParallelAsync(string keyValue, string destinationEntity, string restCredentialName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Versione thread-safe per operazioni parallele - Trova associazione per valore chiave (solo keyValue)
|
||||||
|
/// </summary>
|
||||||
|
Task<KeyAssociation?> FindAssociationByKeyValueParallelAsync(string keyValue);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Versione thread-safe per operazioni parallele - Elimina associazione
|
||||||
|
/// </summary>
|
||||||
|
Task<bool> DeleteAssociationParallelAsync(int id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -23,62 +23,418 @@ public class KeyAssociationService : IKeyAssociationService
|
|||||||
|
|
||||||
public async Task<int> SaveAssociationAsync(KeyAssociation association)
|
public async Task<int> SaveAssociationAsync(KeyAssociation association)
|
||||||
{
|
{
|
||||||
|
// Cattura i valori critici all'inizio per evitare race conditions
|
||||||
|
var keyValue = association.KeyValue;
|
||||||
|
var destinationEntity = association.DestinationEntity;
|
||||||
|
var destinationId = association.DestinationId;
|
||||||
|
var restCredentialName = association.RestCredentialName;
|
||||||
|
var sourceKeyField = association.SourceKeyField;
|
||||||
|
var destinationKeyField = association.DestinationKeyField;
|
||||||
|
var additionalInfo = association.AdditionalInfo;
|
||||||
|
var currentTime = DateTime.UtcNow;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("DEBUG: Tentativo salvataggio associazione - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', DestinationId: '{DestinationId}', RestCredentialName: '{RestCredentialName}'",
|
_logger.LogInformation("DEBUG: Tentativo salvataggio associazione - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', DestinationId: '{DestinationId}', RestCredentialName: '{RestCredentialName}'",
|
||||||
association.KeyValue, association.DestinationEntity, association.DestinationId, association.RestCredentialName);
|
keyValue, destinationEntity, destinationId, restCredentialName);
|
||||||
|
|
||||||
// Controlla se esiste già un'associazione per questo valore chiave e destinazione
|
// Implementazione thread-safe usando upsert pattern
|
||||||
var existing = await _context.KeyAssociations
|
// Prima tenta di aggiornare un record esistente
|
||||||
.FirstOrDefaultAsync(ka =>
|
var rowsAffected = await _context.Database.ExecuteSqlRawAsync(@"
|
||||||
ka.KeyValue == association.KeyValue &&
|
UPDATE KeyAssociations
|
||||||
ka.DestinationEntity == association.DestinationEntity &&
|
SET DestinationId = {0},
|
||||||
ka.RestCredentialName == association.RestCredentialName &&
|
SourceKeyField = {1},
|
||||||
ka.IsActive);
|
DestinationKeyField = {2},
|
||||||
|
UpdatedAt = {3},
|
||||||
_logger.LogInformation("DEBUG: Controllo associazione esistente: {Found}. ID: {Id}",
|
LastVerifiedAt = {4},
|
||||||
existing != null, existing?.Id);
|
AdditionalInfo = {5}
|
||||||
|
WHERE KeyValue = {6}
|
||||||
|
AND DestinationEntity = {7}
|
||||||
|
AND RestCredentialName = {8}
|
||||||
|
AND IsActive = 1",
|
||||||
|
destinationId, sourceKeyField, destinationKeyField, currentTime, currentTime, additionalInfo ?? (object)DBNull.Value,
|
||||||
|
keyValue, destinationEntity, restCredentialName);
|
||||||
|
|
||||||
if (existing != null)
|
if (rowsAffected > 0)
|
||||||
{
|
{
|
||||||
// Aggiorna l'associazione esistente
|
// Recupera l'ID dell'associazione aggiornata
|
||||||
existing.DestinationId = association.DestinationId;
|
var existing = await _context.KeyAssociations
|
||||||
existing.SourceKeyField = association.SourceKeyField;
|
.FirstOrDefaultAsync(ka =>
|
||||||
existing.DestinationKeyField = association.DestinationKeyField;
|
ka.KeyValue == keyValue &&
|
||||||
existing.UpdatedAt = DateTime.UtcNow;
|
ka.DestinationEntity == destinationEntity &&
|
||||||
existing.LastVerifiedAt = DateTime.UtcNow;
|
ka.RestCredentialName == restCredentialName &&
|
||||||
existing.AdditionalInfo = association.AdditionalInfo;
|
ka.IsActive);
|
||||||
|
|
||||||
// Aggiorna le informazioni sulle sorgenti
|
|
||||||
UpdateSourcesInfo(existing, association);
|
|
||||||
|
|
||||||
_context.KeyAssociations.Update(existing);
|
if (existing != null)
|
||||||
await _context.SaveChangesAsync();
|
{
|
||||||
|
// Aggiorna le informazioni sulle sorgenti usando l'entità tracciata
|
||||||
|
UpdateSourcesInfo(existing, association);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
_logger.LogInformation("Associazione aggiornata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}",
|
_logger.LogInformation("Associazione aggiornata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}",
|
||||||
association.KeyValue, association.DestinationEntity, association.DestinationId);
|
keyValue, destinationEntity, destinationId);
|
||||||
|
|
||||||
return existing.Id;
|
return existing.Id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
// Se l'aggiornamento non ha modificato nessuna riga, tenta l'inserimento
|
||||||
|
try
|
||||||
{
|
{
|
||||||
// Crea nuova associazione
|
var newAssociation = new KeyAssociation
|
||||||
association.CreatedAt = DateTime.UtcNow;
|
{
|
||||||
association.LastVerifiedAt = DateTime.UtcNow;
|
KeyValue = keyValue,
|
||||||
|
SourceKeyField = sourceKeyField,
|
||||||
|
DestinationKeyField = destinationKeyField,
|
||||||
|
DestinationEntity = destinationEntity,
|
||||||
|
DestinationId = destinationId,
|
||||||
|
RestCredentialName = restCredentialName,
|
||||||
|
CreatedAt = currentTime,
|
||||||
|
LastVerifiedAt = currentTime,
|
||||||
|
AdditionalInfo = additionalInfo,
|
||||||
|
IsActive = true
|
||||||
|
};
|
||||||
|
|
||||||
_context.KeyAssociations.Add(association);
|
_context.KeyAssociations.Add(newAssociation);
|
||||||
await _context.SaveChangesAsync();
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
_logger.LogInformation("Nuova associazione creata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}",
|
_logger.LogInformation("Nuova associazione creata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}",
|
||||||
association.KeyValue, association.DestinationEntity, association.DestinationId);
|
keyValue, destinationEntity, destinationId);
|
||||||
|
|
||||||
return association.Id;
|
return newAssociation.Id;
|
||||||
|
}
|
||||||
|
catch (Microsoft.EntityFrameworkCore.DbUpdateException dbEx) when (dbEx.InnerException?.Message?.Contains("UNIQUE constraint failed") == true)
|
||||||
|
{
|
||||||
|
// Race condition: un altro thread ha inserito la stessa associazione
|
||||||
|
// Ritenta la ricerca e aggiornamento
|
||||||
|
_logger.LogDebug("Race condition rilevata durante inserimento, ritento con aggiornamento per KeyValue: {KeyValue}", keyValue);
|
||||||
|
|
||||||
|
var existing = await _context.KeyAssociations
|
||||||
|
.FirstOrDefaultAsync(ka =>
|
||||||
|
ka.KeyValue == keyValue &&
|
||||||
|
ka.DestinationEntity == destinationEntity &&
|
||||||
|
ka.RestCredentialName == restCredentialName &&
|
||||||
|
ka.IsActive);
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
// Aggiorna l'associazione trovata
|
||||||
|
existing.DestinationId = destinationId;
|
||||||
|
existing.SourceKeyField = sourceKeyField;
|
||||||
|
existing.DestinationKeyField = destinationKeyField;
|
||||||
|
existing.UpdatedAt = currentTime;
|
||||||
|
existing.LastVerifiedAt = currentTime;
|
||||||
|
existing.AdditionalInfo = additionalInfo;
|
||||||
|
|
||||||
|
UpdateSourcesInfo(existing, association);
|
||||||
|
|
||||||
|
_context.KeyAssociations.Update(existing);
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogInformation("Associazione aggiornata dopo race condition: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}",
|
||||||
|
keyValue, destinationEntity, destinationId);
|
||||||
|
|
||||||
|
return existing.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se non riusciamo a trovare neanche l'associazione creata da un altro thread, rilancia l'eccezione
|
||||||
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Errore nel salvare l'associazione: KeyValue={KeyValue} -> {DestinationEntity}",
|
_logger.LogError(ex, "Errore nel salvare l'associazione: KeyValue={KeyValue} -> {DestinationEntity}",
|
||||||
association.KeyValue, association.DestinationEntity);
|
keyValue, destinationEntity);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Versione thread-safe del SaveAssociationAsync che utilizza un DbContext separato per operazioni parallele
|
||||||
|
/// </summary>
|
||||||
|
public async Task<int> SaveAssociationParallelAsync(KeyAssociation association)
|
||||||
|
{
|
||||||
|
// Cattura i valori critici all'inizio per evitare race conditions
|
||||||
|
var keyValue = association.KeyValue;
|
||||||
|
var destinationEntity = association.DestinationEntity;
|
||||||
|
var destinationId = association.DestinationId;
|
||||||
|
var restCredentialName = association.RestCredentialName;
|
||||||
|
var sourceKeyField = association.SourceKeyField;
|
||||||
|
var destinationKeyField = association.DestinationKeyField;
|
||||||
|
var additionalInfo = association.AdditionalInfo;
|
||||||
|
var currentTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Crea un nuovo DbContext per questa operazione parallela
|
||||||
|
var options = new DbContextOptionsBuilder<CredentialDbContext>()
|
||||||
|
.UseSqlite(_context.Database.GetConnectionString())
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
using var parallelContext = new CredentialDbContext(options);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogDebug("PARALLEL: Tentativo salvataggio associazione - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', DestinationId: '{DestinationId}', RestCredentialName: '{RestCredentialName}'",
|
||||||
|
keyValue, destinationEntity, destinationId, restCredentialName);
|
||||||
|
|
||||||
|
// Implementazione thread-safe usando upsert pattern con DbContext separato
|
||||||
|
// Prima tenta di aggiornare un record esistente
|
||||||
|
var rowsAffected = await parallelContext.Database.ExecuteSqlRawAsync(@"
|
||||||
|
UPDATE KeyAssociations
|
||||||
|
SET DestinationId = {0},
|
||||||
|
SourceKeyField = {1},
|
||||||
|
DestinationKeyField = {2},
|
||||||
|
UpdatedAt = {3},
|
||||||
|
LastVerifiedAt = {4},
|
||||||
|
AdditionalInfo = {5}
|
||||||
|
WHERE KeyValue = {6}
|
||||||
|
AND DestinationEntity = {7}
|
||||||
|
AND RestCredentialName = {8}
|
||||||
|
AND IsActive = 1",
|
||||||
|
destinationId, sourceKeyField, destinationKeyField, currentTime, currentTime, additionalInfo ?? (object)DBNull.Value,
|
||||||
|
keyValue, destinationEntity, restCredentialName);
|
||||||
|
|
||||||
|
if (rowsAffected > 0)
|
||||||
|
{
|
||||||
|
// Recupera l'ID dell'associazione aggiornata
|
||||||
|
var existing = await parallelContext.KeyAssociations
|
||||||
|
.FirstOrDefaultAsync(ka =>
|
||||||
|
ka.KeyValue == keyValue &&
|
||||||
|
ka.DestinationEntity == destinationEntity &&
|
||||||
|
ka.RestCredentialName == restCredentialName &&
|
||||||
|
ka.IsActive);
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
// Aggiorna le informazioni sulle sorgenti usando l'entità tracciata
|
||||||
|
UpdateSourcesInfo(existing, association);
|
||||||
|
await parallelContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogDebug("PARALLEL: Associazione aggiornata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}",
|
||||||
|
keyValue, destinationEntity, destinationId);
|
||||||
|
|
||||||
|
return existing.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se l'aggiornamento non ha modificato nessuna riga, tenta l'inserimento
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var newAssociation = new KeyAssociation
|
||||||
|
{
|
||||||
|
KeyValue = keyValue,
|
||||||
|
SourceKeyField = sourceKeyField,
|
||||||
|
DestinationKeyField = destinationKeyField,
|
||||||
|
DestinationEntity = destinationEntity,
|
||||||
|
DestinationId = destinationId,
|
||||||
|
RestCredentialName = restCredentialName,
|
||||||
|
CreatedAt = currentTime,
|
||||||
|
LastVerifiedAt = currentTime,
|
||||||
|
AdditionalInfo = additionalInfo,
|
||||||
|
IsActive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
parallelContext.KeyAssociations.Add(newAssociation);
|
||||||
|
await parallelContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogDebug("PARALLEL: Nuova associazione creata: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}",
|
||||||
|
keyValue, destinationEntity, destinationId);
|
||||||
|
|
||||||
|
return newAssociation.Id;
|
||||||
|
}
|
||||||
|
catch (Microsoft.EntityFrameworkCore.DbUpdateException dbEx) when (dbEx.InnerException?.Message?.Contains("UNIQUE constraint failed") == true)
|
||||||
|
{
|
||||||
|
// Race condition: un altro thread ha inserito la stessa associazione
|
||||||
|
// Ritenta la ricerca e aggiornamento
|
||||||
|
_logger.LogDebug("PARALLEL: Race condition rilevata durante inserimento, ritento con aggiornamento per KeyValue: {KeyValue}", keyValue);
|
||||||
|
|
||||||
|
var existing = await parallelContext.KeyAssociations
|
||||||
|
.FirstOrDefaultAsync(ka =>
|
||||||
|
ka.KeyValue == keyValue &&
|
||||||
|
ka.DestinationEntity == destinationEntity &&
|
||||||
|
ka.RestCredentialName == restCredentialName &&
|
||||||
|
ka.IsActive);
|
||||||
|
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
// Aggiorna l'associazione trovata
|
||||||
|
existing.DestinationId = destinationId;
|
||||||
|
existing.SourceKeyField = sourceKeyField;
|
||||||
|
existing.DestinationKeyField = destinationKeyField;
|
||||||
|
existing.UpdatedAt = currentTime;
|
||||||
|
existing.LastVerifiedAt = currentTime;
|
||||||
|
existing.AdditionalInfo = additionalInfo;
|
||||||
|
|
||||||
|
UpdateSourcesInfo(existing, association);
|
||||||
|
|
||||||
|
parallelContext.KeyAssociations.Update(existing);
|
||||||
|
await parallelContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogDebug("PARALLEL: Associazione aggiornata dopo race condition: KeyValue={KeyValue} -> {DestinationEntity}/{DestinationId}",
|
||||||
|
keyValue, destinationEntity, destinationId);
|
||||||
|
|
||||||
|
return existing.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Se non riusciamo a trovare neanche l'associazione creata da un altro thread, rilancia l'eccezione
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "PARALLEL: Errore nel salvare l'associazione: KeyValue={KeyValue} -> {DestinationEntity}",
|
||||||
|
keyValue, destinationEntity);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Versione thread-safe per operazioni parallele - Find association by key value
|
||||||
|
/// </summary>
|
||||||
|
public async Task<KeyAssociation?> FindAssociationByKeyValueParallelAsync(string keyValue, string destinationEntity, string restCredentialName)
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<CredentialDbContext>()
|
||||||
|
.UseSqlite(_context.Database.GetConnectionString())
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
using var parallelContext = new CredentialDbContext(options);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogDebug("PARALLEL: Ricerca associazione con parametri - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', RestCredentialName: '{RestCredentialName}'",
|
||||||
|
keyValue, destinationEntity, restCredentialName);
|
||||||
|
|
||||||
|
var result = await parallelContext.KeyAssociations
|
||||||
|
.FirstOrDefaultAsync(ka =>
|
||||||
|
ka.KeyValue == keyValue &&
|
||||||
|
ka.DestinationEntity == destinationEntity &&
|
||||||
|
ka.RestCredentialName == restCredentialName &&
|
||||||
|
ka.IsActive);
|
||||||
|
|
||||||
|
_logger.LogDebug("PARALLEL: Risultato ricerca associazione: {Found}. ID: {Id}, DestinationId: '{DestinationId}'",
|
||||||
|
result != null, result?.Id, result?.DestinationId);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "PARALLEL: Errore nella ricerca dell'associazione: KeyValue={KeyValue} -> {DestinationEntity}",
|
||||||
|
keyValue, destinationEntity);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Versione thread-safe per operazioni parallele - Find association by key value only
|
||||||
|
/// </summary>
|
||||||
|
public async Task<KeyAssociation?> FindAssociationByKeyValueParallelAsync(string keyValue)
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<CredentialDbContext>()
|
||||||
|
.UseSqlite(_context.Database.GetConnectionString())
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
using var parallelContext = new CredentialDbContext(options);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await parallelContext.KeyAssociations
|
||||||
|
.Where(ka => ka.KeyValue == keyValue && ka.IsActive)
|
||||||
|
.OrderByDescending(ka => ka.UpdatedAt ?? ka.CreatedAt)
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "PARALLEL: Errore nella ricerca dell'associazione per KeyValue={KeyValue}", keyValue);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Versione thread-safe per operazioni parallele - Delete association
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> DeleteAssociationParallelAsync(int id)
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<CredentialDbContext>()
|
||||||
|
.UseSqlite(_context.Database.GetConnectionString())
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
using var parallelContext = new CredentialDbContext(options);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var association = await parallelContext.KeyAssociations.FindAsync(id);
|
||||||
|
if (association == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("PARALLEL: Associazione con ID {Id} non trovata per l'eliminazione", id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
parallelContext.KeyAssociations.Remove(association);
|
||||||
|
await parallelContext.SaveChangesAsync();
|
||||||
|
|
||||||
|
_logger.LogDebug("PARALLEL: Associazione eliminata: ID {Id}", id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "PARALLEL: Errore nell'eliminazione dell'associazione: ID {Id}", id);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Versione thread-safe per operazioni parallele - Salva una associazione con parametri semplici
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> SaveAssociationParallelAsync(string keyValue, string destinationEntity, string destinationId, string restCredentialName)
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<CredentialDbContext>()
|
||||||
|
.UseSqlite(_context.Database.GetConnectionString())
|
||||||
|
.Options;
|
||||||
|
|
||||||
|
using var parallelContext = new CredentialDbContext(options);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogDebug("PARALLEL: Salvataggio associazione - KeyValue: '{KeyValue}', DestinationEntity: '{DestinationEntity}', DestinationId: '{DestinationId}', RestCredentialName: '{RestCredentialName}'",
|
||||||
|
keyValue, destinationEntity, destinationId, restCredentialName);
|
||||||
|
|
||||||
|
var currentTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Implementazione thread-safe usando upsert pattern
|
||||||
|
var rowsAffected = await parallelContext.Database.ExecuteSqlRawAsync(@"
|
||||||
|
UPDATE KeyAssociations
|
||||||
|
SET DestinationId = {0},
|
||||||
|
UpdatedAt = {1},
|
||||||
|
LastVerifiedAt = {2},
|
||||||
|
IsActive = 1
|
||||||
|
WHERE KeyValue = {3}
|
||||||
|
AND DestinationEntity = {4}
|
||||||
|
AND RestCredentialName = {5}",
|
||||||
|
destinationId, currentTime, currentTime, keyValue, destinationEntity, restCredentialName);
|
||||||
|
|
||||||
|
if (rowsAffected == 0)
|
||||||
|
{
|
||||||
|
// Se nessun record è stato aggiornato, inserisci un nuovo record
|
||||||
|
await parallelContext.Database.ExecuteSqlRawAsync(@"
|
||||||
|
INSERT INTO KeyAssociations
|
||||||
|
(KeyValue, DestinationEntity, DestinationId, RestCredentialName, CreatedAt, UpdatedAt, LastVerifiedAt, IsActive)
|
||||||
|
VALUES ({0}, {1}, {2}, {3}, {4}, {5}, {6}, 1)",
|
||||||
|
keyValue, destinationEntity, destinationId, restCredentialName, currentTime, currentTime, currentTime);
|
||||||
|
|
||||||
|
_logger.LogDebug("PARALLEL: Nuova associazione creata: {KeyValue} -> {DestinationEntity}({DestinationId})",
|
||||||
|
keyValue, destinationEntity, destinationId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogDebug("PARALLEL: Associazione aggiornata: {KeyValue} -> {DestinationEntity}({DestinationId})",
|
||||||
|
keyValue, destinationEntity, destinationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "PARALLEL: Errore nel salvare l'associazione: KeyValue={KeyValue} -> {DestinationEntity}",
|
||||||
|
keyValue, destinationEntity);
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ public interface IDataConnectionCredentialService
|
|||||||
|
|
||||||
// Key associations
|
// Key associations
|
||||||
Task<int> SaveKeyAssociationAsync(KeyAssociation association);
|
Task<int> SaveKeyAssociationAsync(KeyAssociation association);
|
||||||
|
Task<int> SaveKeyAssociationParallelAsync(KeyAssociation association);
|
||||||
Task<KeyAssociation?> FindKeyAssociationByValueAsync(string keyValue, string destinationEntity, string restCredentialName);
|
Task<KeyAssociation?> FindKeyAssociationByValueAsync(string keyValue, string destinationEntity, string restCredentialName);
|
||||||
Task<KeyAssociation?> FindKeyAssociationByValueAsync(string keyValue);
|
Task<KeyAssociation?> FindKeyAssociationByValueAsync(string keyValue);
|
||||||
Task<List<KeyAssociation>> GetKeyAssociationsByDestinationAsync(string destinationEntity, string restCredentialName);
|
Task<List<KeyAssociation>> GetKeyAssociationsByDestinationAsync(string destinationEntity, string restCredentialName);
|
||||||
@@ -77,4 +78,10 @@ public interface IDataConnectionCredentialService
|
|||||||
Task<int> CleanupInvalidKeyAssociationsAsync(string destinationEntity, string restCredentialName);
|
Task<int> CleanupInvalidKeyAssociationsAsync(string destinationEntity, string restCredentialName);
|
||||||
Task<bool> UpdateKeyAssociationLastVerifiedAsync(int id);
|
Task<bool> UpdateKeyAssociationLastVerifiedAsync(int id);
|
||||||
Task<AssociationStatistics> GetKeyAssociationStatisticsAsync();
|
Task<AssociationStatistics> GetKeyAssociationStatisticsAsync();
|
||||||
|
|
||||||
|
// Parallel key association operations
|
||||||
|
Task<bool> SaveKeyAssociationParallelAsync(string keyValue, string destinationEntity, string destinationId, string restCredentialName);
|
||||||
|
Task<KeyAssociation?> FindKeyAssociationByValueParallelAsync(string keyValue, string destinationEntity, string restCredentialName);
|
||||||
|
Task<KeyAssociation?> FindKeyAssociationByValueParallelAsync(string keyValue);
|
||||||
|
Task<bool> DeleteKeyAssociationParallelAsync(int id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -866,6 +866,11 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
|
|||||||
return await _keyAssociationService.SaveAssociationAsync(association);
|
return await _keyAssociationService.SaveAssociationAsync(association);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<int> SaveKeyAssociationParallelAsync(KeyAssociation association)
|
||||||
|
{
|
||||||
|
return await _keyAssociationService.SaveAssociationParallelAsync(association);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<KeyAssociation?> FindKeyAssociationByValueAsync(string keyValue, string destinationEntity, string restCredentialName)
|
public async Task<KeyAssociation?> FindKeyAssociationByValueAsync(string keyValue, string destinationEntity, string restCredentialName)
|
||||||
{
|
{
|
||||||
return await _keyAssociationService.FindAssociationByKeyValueAsync(keyValue, destinationEntity, restCredentialName);
|
return await _keyAssociationService.FindAssociationByKeyValueAsync(keyValue, destinationEntity, restCredentialName);
|
||||||
@@ -936,6 +941,27 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
|
|||||||
return await _keyAssociationService.GetStatisticsAsync();
|
return await _keyAssociationService.GetStatisticsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parallel key association operations
|
||||||
|
public async Task<bool> SaveKeyAssociationParallelAsync(string keyValue, string destinationEntity, string destinationId, string restCredentialName)
|
||||||
|
{
|
||||||
|
return await _keyAssociationService.SaveAssociationParallelAsync(keyValue, destinationEntity, destinationId, restCredentialName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<KeyAssociation?> FindKeyAssociationByValueParallelAsync(string keyValue, string destinationEntity, string restCredentialName)
|
||||||
|
{
|
||||||
|
return await _keyAssociationService.FindAssociationByKeyValueParallelAsync(keyValue, destinationEntity, restCredentialName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<KeyAssociation?> FindKeyAssociationByValueParallelAsync(string keyValue)
|
||||||
|
{
|
||||||
|
return await _keyAssociationService.FindAssociationByKeyValueParallelAsync(keyValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> DeleteKeyAssociationParallelAsync(int id)
|
||||||
|
{
|
||||||
|
return await _keyAssociationService.DeleteAssociationParallelAsync(id);
|
||||||
|
}
|
||||||
|
|
||||||
#region Helper Methods
|
#region Helper Methods
|
||||||
|
|
||||||
public async Task<int?> GetCredentialIdByNameAsync(string name, CredentialManager.Models.CredentialType type)
|
public async Task<int?> GetCredentialIdByNameAsync(string name, CredentialManager.Models.CredentialType type)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@page "/data-coupler"
|
@page "/"
|
||||||
@using CredentialManager.Models
|
@using CredentialManager.Models
|
||||||
@using DataConnection.Interfaces
|
@using DataConnection.Interfaces
|
||||||
@using DataConnection.CredentialManagement.Interfaces
|
@using DataConnection.CredentialManagement.Interfaces
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using CredentialManager.Models;
|
using CredentialManager.Models;
|
||||||
@@ -2083,14 +2084,6 @@ public partial class DataCoupler : ComponentBase
|
|||||||
StateHasChanged();
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ottiene l'ID della credenziale sorgente corrente
|
/// Ottiene l'ID della credenziale sorgente corrente
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -2504,40 +2497,55 @@ public partial class DataCoupler : ComponentBase
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Trasforma i record e analizza le associazioni
|
// 2. Trasforma i record e analizza le associazioni IN PARALLELO
|
||||||
var recordsForCreate = new List<(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 List<(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)>();
|
||||||
var invalidAssociations = new List<int>(); // IDs delle associazioni da eliminare
|
var recordErrors = new ConcurrentBag<TransferResult>();
|
||||||
|
|
||||||
int recordNumber = 1;
|
// Cattura i valori condivisi per evitare race conditions
|
||||||
foreach (var record in records)
|
var currentEntityName = selectedRestEntity.Name;
|
||||||
|
var currentCredentialName = selectedRestCredential;
|
||||||
|
var currentUseRecordAssociations = useRecordAssociations;
|
||||||
|
|
||||||
|
// Crea lista indicizzata per mantenere il record number
|
||||||
|
var indexedRecords = records.Select((record, index) => new { Record = record, RecordNumber = index + 1 }).ToList();
|
||||||
|
|
||||||
|
Logger.LogInformation("COMPOSITE: Inizio analisi parallela di {RecordCount} record", indexedRecords.Count);
|
||||||
|
var analysisStartTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// Processa tutti i record in parallelo
|
||||||
|
var processingTasks = indexedRecords.Select(async indexedRecord =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Trasforma il record in base ai mapping
|
var record = indexedRecord.Record;
|
||||||
|
var recordNumber = indexedRecord.RecordNumber;
|
||||||
|
|
||||||
|
// 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
|
// Genera la chiave sorgente per questo record (operazione locale, thread-safe)
|
||||||
var sourceKey = GenerateSourceKey(record);
|
var sourceKey = GenerateSourceKey(record);
|
||||||
|
|
||||||
// Analizza le associazioni per capire se aggiornare o creare
|
// Analizza le associazioni per capire se aggiornare o creare
|
||||||
if (useRecordAssociations && !string.IsNullOrEmpty(sourceKey))
|
if (currentUseRecordAssociations && !string.IsNullOrEmpty(sourceKey))
|
||||||
{
|
{
|
||||||
Logger.LogDebug("COMPOSITE: Cerco associazione per KeyValue: '{KeyValue}', Entity: '{Entity}', Credential: '{Credential}'",
|
Logger.LogDebug("COMPOSITE PARALLEL: Cerco associazione per KeyValue: '{KeyValue}', Entity: '{Entity}', Credential: '{Credential}'",
|
||||||
sourceKey, selectedRestEntity.Name, selectedRestCredential);
|
sourceKey, currentEntityName, currentCredentialName);
|
||||||
|
|
||||||
var existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(
|
// Usa i metodi paralleli per le operazioni di database
|
||||||
sourceKey, selectedRestEntity.Name, selectedRestCredential);
|
var existingAssociation = await CredentialService.FindKeyAssociationByValueParallelAsync(
|
||||||
|
sourceKey, currentEntityName, currentCredentialName);
|
||||||
|
|
||||||
// FALLBACK: Se non troviamo l'associazione con tutti i parametri, proviamo solo con il KeyValue
|
// FALLBACK: Se non troviamo l'associazione con tutti i parametri, proviamo solo con il KeyValue
|
||||||
if (existingAssociation == null)
|
if (existingAssociation == null)
|
||||||
{
|
{
|
||||||
existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(sourceKey);
|
existingAssociation = await CredentialService.FindKeyAssociationByValueParallelAsync(sourceKey);
|
||||||
if (existingAssociation != null)
|
if (existingAssociation != null)
|
||||||
{
|
{
|
||||||
// Verifica compatibilità
|
// Verifica compatibilità
|
||||||
if (existingAssociation.DestinationEntity != selectedRestEntity.Name ||
|
if (existingAssociation.DestinationEntity != currentEntityName ||
|
||||||
existingAssociation.RestCredentialName != selectedRestCredential)
|
existingAssociation.RestCredentialName != currentCredentialName)
|
||||||
{
|
{
|
||||||
existingAssociation = null;
|
existingAssociation = null;
|
||||||
}
|
}
|
||||||
@@ -2548,50 +2556,68 @@ public partial class DataCoupler : ComponentBase
|
|||||||
{
|
{
|
||||||
// Record da aggiornare
|
// Record da aggiornare
|
||||||
recordsForUpdate.Add((restData, existingAssociation.DestinationId, record, recordNumber));
|
recordsForUpdate.Add((restData, existingAssociation.DestinationId, record, recordNumber));
|
||||||
|
Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} marcato per aggiornamento (EntityId: {EntityId})",
|
||||||
|
recordNumber, existingAssociation.DestinationId);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Record da creare
|
// Record da creare
|
||||||
recordsForCreate.Add((restData, record, recordNumber));
|
recordsForCreate.Add((restData, record, recordNumber));
|
||||||
|
Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} marcato per creazione", recordNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Record da creare (no associazioni)
|
// Record da creare (no associazioni)
|
||||||
recordsForCreate.Add((restData, record, recordNumber));
|
recordsForCreate.Add((restData, record, recordNumber));
|
||||||
|
Logger.LogDebug("COMPOSITE PARALLEL: Record {RecordNumber} marcato per creazione (no associazioni)", recordNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Logger.LogError(ex, "Errore nella trasformazione del record {RecordNumber}", recordNumber);
|
Logger.LogError(ex, "COMPOSITE PARALLEL: Errore nella trasformazione del record {RecordNumber}", indexedRecord.RecordNumber);
|
||||||
transferResults.Add(new TransferResult
|
recordErrors.Add(new TransferResult
|
||||||
{
|
{
|
||||||
RecordNumber = recordNumber,
|
RecordNumber = indexedRecord.RecordNumber,
|
||||||
RecordData = new Dictionary<string, object>(record),
|
RecordData = new Dictionary<string, object>(indexedRecord.Record),
|
||||||
Status = "error",
|
Status = "error",
|
||||||
Message = $"Errore trasformazione: {ex.Message}"
|
Message = $"Errore trasformazione: {ex.Message}"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
recordNumber++;
|
// Attendi il completamento di tutte le operazioni parallele
|
||||||
|
await Task.WhenAll(processingTasks);
|
||||||
|
|
||||||
|
var analysisEndTime = DateTime.UtcNow;
|
||||||
|
var analysisElapsed = (analysisEndTime - analysisStartTime).TotalMilliseconds;
|
||||||
|
|
||||||
|
// Aggiungi gli errori ai risultati di trasferimento
|
||||||
|
foreach (var error in recordErrors)
|
||||||
|
{
|
||||||
|
transferResults.Add(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.LogInformation("COMPOSITE: Analisi completata - {CreateCount} record da creare, {UpdateCount} record da aggiornare",
|
// Converti i ConcurrentBag in liste per il resto del processing
|
||||||
recordsForCreate.Count, recordsForUpdate.Count);
|
var finalRecordsForCreate = recordsForCreate.ToList();
|
||||||
|
var finalRecordsForUpdate = recordsForUpdate.ToList();
|
||||||
|
|
||||||
|
Logger.LogInformation("COMPOSITE: Analisi parallela completata in {ElapsedMs}ms - {CreateCount} record da creare, {UpdateCount} record da aggiornare, {ErrorCount} errori",
|
||||||
|
analysisElapsed, finalRecordsForCreate.Count, finalRecordsForUpdate.Count, recordErrors.Count);
|
||||||
|
|
||||||
// 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>());
|
||||||
var updateTask = Task.FromResult(new List<DataConnection.REST.Implementations.SalesforceServiceClient.CompositeOperationResult>());
|
var updateTask = Task.FromResult(new List<DataConnection.REST.Implementations.SalesforceServiceClient.CompositeOperationResult>());
|
||||||
|
|
||||||
if (recordsForCreate.Any())
|
if (finalRecordsForCreate.Any())
|
||||||
{
|
{
|
||||||
var createData = recordsForCreate.Select(r => r.transformedData).ToList();
|
var createData = finalRecordsForCreate.Select(r => r.transformedData).ToList();
|
||||||
createTask = salesforceClient.BatchCreateEntitiesAsync(selectedRestEntity.Name, createData);
|
createTask = salesforceClient.BatchCreateEntitiesAsync(selectedRestEntity.Name, createData);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recordsForUpdate.Any())
|
if (finalRecordsForUpdate.Any())
|
||||||
{
|
{
|
||||||
var updateData = recordsForUpdate.ToDictionary(
|
var updateData = finalRecordsForUpdate.ToDictionary(
|
||||||
r => r.entityId,
|
r => r.entityId,
|
||||||
r => r.transformedData);
|
r => r.transformedData);
|
||||||
updateTask = salesforceClient.BatchUpdateEntitiesAsync(selectedRestEntity.Name, updateData);
|
updateTask = salesforceClient.BatchUpdateEntitiesAsync(selectedRestEntity.Name, updateData);
|
||||||
@@ -2608,10 +2634,13 @@ public partial class DataCoupler : ComponentBase
|
|||||||
int errorCount = 0;
|
int errorCount = 0;
|
||||||
int updatedCount = 0;
|
int updatedCount = 0;
|
||||||
|
|
||||||
|
// Lista per raccogliere le task di creazione associazioni
|
||||||
|
var createAssociationTasks = new List<Task>();
|
||||||
|
|
||||||
for (int i = 0; i < createResults.Count; i++)
|
for (int i = 0; i < createResults.Count; i++)
|
||||||
{
|
{
|
||||||
var result = createResults[i];
|
var result = createResults[i];
|
||||||
var originalData = recordsForCreate[i];
|
var originalData = finalRecordsForCreate[i];
|
||||||
|
|
||||||
var transferResult = new TransferResult
|
var transferResult = new TransferResult
|
||||||
{
|
{
|
||||||
@@ -2626,10 +2655,12 @@ public partial class DataCoupler : ComponentBase
|
|||||||
transferResult.Message = "Record inserito con successo (Composite)";
|
transferResult.Message = "Record inserito con successo (Composite)";
|
||||||
transferResult.EntityId = result.EntityId;
|
transferResult.EntityId = result.EntityId;
|
||||||
|
|
||||||
// Crea associazione se necessario
|
// Aggiungi task di creazione associazione alla lista (esecuzione parallela)
|
||||||
if (useRecordAssociations && !string.IsNullOrEmpty(transferResult.EntityId))
|
if (useRecordAssociations && !string.IsNullOrEmpty(transferResult.EntityId))
|
||||||
{
|
{
|
||||||
await CreateAssociationAsync(originalData.originalRecord, transferResult.EntityId, originalData.recordNumber);
|
// IMPORTANTE: Non awaita qui, solo crea il task per esecuzione parallela
|
||||||
|
var associationTask = CreateAssociationAsync(originalData.originalRecord, transferResult.EntityId, originalData.recordNumber);
|
||||||
|
createAssociationTasks.Add(associationTask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -2643,10 +2674,13 @@ public partial class DataCoupler : ComponentBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. Processa i risultati degli aggiornamenti
|
// 5. Processa i risultati degli aggiornamenti
|
||||||
|
// Lista per raccogliere le task di aggiornamento associazioni
|
||||||
|
var updateAssociationTasks = new List<Task>();
|
||||||
|
|
||||||
for (int i = 0; i < updateResults.Count; i++)
|
for (int i = 0; i < updateResults.Count; i++)
|
||||||
{
|
{
|
||||||
var result = updateResults[i];
|
var result = updateResults[i];
|
||||||
var originalData = recordsForUpdate[i];
|
var originalData = finalRecordsForUpdate[i];
|
||||||
|
|
||||||
var transferResult = new TransferResult
|
var transferResult = new TransferResult
|
||||||
{
|
{
|
||||||
@@ -2661,10 +2695,12 @@ public partial class DataCoupler : ComponentBase
|
|||||||
transferResult.Message = $"Record aggiornato con successo (Composite) - ID: {result.EntityId}";
|
transferResult.Message = $"Record aggiornato con successo (Composite) - ID: {result.EntityId}";
|
||||||
transferResult.EntityId = result.EntityId;
|
transferResult.EntityId = result.EntityId;
|
||||||
|
|
||||||
// Aggiorna l'associazione
|
// Aggiungi task di aggiornamento associazione alla lista (esecuzione parallela)
|
||||||
if (useRecordAssociations && !string.IsNullOrEmpty(result.EntityId))
|
if (useRecordAssociations && !string.IsNullOrEmpty(result.EntityId))
|
||||||
{
|
{
|
||||||
await UpdateAssociationVerificationAsync(result.EntityId);
|
// IMPORTANTE: Non awaita qui, solo crea il task per esecuzione parallela
|
||||||
|
var verificationTask = UpdateAssociationVerificationAsync(result.EntityId);
|
||||||
|
updateAssociationTasks.Add(verificationTask);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -2673,17 +2709,38 @@ 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}";
|
||||||
|
|
||||||
// Elimina associazione non valida se l'aggiornamento fallisce
|
// Aggiungi task di gestione fallimento alla lista (esecuzione parallela)
|
||||||
if (useRecordAssociations)
|
if (useRecordAssociations)
|
||||||
{
|
{
|
||||||
await HandleFailedUpdateAsync(originalData.originalRecord, originalData.recordNumber);
|
// 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Mostra risultati
|
// 6. Esegui tutte le operazioni di associazione in parallelo
|
||||||
|
var allAssociationTasks = createAssociationTasks.Concat(updateAssociationTasks).ToList();
|
||||||
|
if (allAssociationTasks.Any())
|
||||||
|
{
|
||||||
|
Logger.LogInformation("COMPOSITE: Avvio di {TaskCount} operazioni di associazione in parallelo ({CreateCount} creazioni, {UpdateCount} aggiornamenti) usando DbContext separati",
|
||||||
|
allAssociationTasks.Count, createAssociationTasks.Count, updateAssociationTasks.Count);
|
||||||
|
|
||||||
|
var startTime = DateTime.UtcNow;
|
||||||
|
await Task.WhenAll(allAssociationTasks);
|
||||||
|
var endTime = DateTime.UtcNow;
|
||||||
|
|
||||||
|
Logger.LogInformation("COMPOSITE: Operazioni di associazione completate in {ElapsedMs}ms con esecuzione parallela reale",
|
||||||
|
(endTime - startTime).TotalMilliseconds);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.LogInformation("COMPOSITE: Nessuna operazione di associazione da eseguire");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. Mostra risultati
|
||||||
ShowTransferResults(successCount, updatedCount, 0, errorCount);
|
ShowTransferResults(successCount, updatedCount, 0, errorCount);
|
||||||
|
|
||||||
Logger.LogInformation("Trasferimento COMPOSITE completato. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Errori: {ErrorCount}",
|
Logger.LogInformation("Trasferimento COMPOSITE completato. Inserimenti: {SuccessCount}, Aggiornamenti: {UpdatedCount}, Errori: {ErrorCount}",
|
||||||
@@ -2705,6 +2762,13 @@ public partial class DataCoupler : ComponentBase
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Cattura i valori condivisi all'inizio per evitare race conditions
|
||||||
|
var currentSourceKeyField = sourceKeyField;
|
||||||
|
var currentEntityName = selectedRestEntity?.Name ?? "";
|
||||||
|
var currentCredentialName = selectedRestCredential ?? "";
|
||||||
|
var currentMappingCount = fieldMappings.Count;
|
||||||
|
var currentSourceType = selectedSourceType;
|
||||||
|
|
||||||
var sourceKey = GenerateSourceKey(originalRecord);
|
var sourceKey = GenerateSourceKey(originalRecord);
|
||||||
if (string.IsNullOrEmpty(sourceKey)) return;
|
if (string.IsNullOrEmpty(sourceKey)) return;
|
||||||
|
|
||||||
@@ -2712,25 +2776,25 @@ public partial class DataCoupler : ComponentBase
|
|||||||
var association = new KeyAssociation
|
var association = new KeyAssociation
|
||||||
{
|
{
|
||||||
KeyValue = sourceKey,
|
KeyValue = sourceKey,
|
||||||
SourceKeyField = sourceKeyField,
|
SourceKeyField = currentSourceKeyField,
|
||||||
DestinationKeyField = destinationKeyField,
|
DestinationKeyField = destinationKeyField,
|
||||||
DestinationEntity = selectedRestEntity?.Name ?? "",
|
DestinationEntity = currentEntityName,
|
||||||
DestinationId = entityId,
|
DestinationId = entityId,
|
||||||
RestCredentialName = selectedRestCredential,
|
RestCredentialName = currentCredentialName,
|
||||||
CreatedAt = DateTime.UtcNow,
|
CreatedAt = DateTime.UtcNow,
|
||||||
LastVerifiedAt = DateTime.UtcNow,
|
LastVerifiedAt = DateTime.UtcNow,
|
||||||
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 = fieldMappings.Count,
|
MappingCount = currentMappingCount,
|
||||||
SourceType = selectedSourceType,
|
SourceType = currentSourceType,
|
||||||
CompositeTransfer = true
|
CompositeTransfer = true
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
var associationId = await CredentialService.SaveKeyAssociationAsync(association);
|
var associationId = await CredentialService.SaveKeyAssociationParallelAsync(association);
|
||||||
Logger.LogDebug("COMPOSITE: Associazione creata con ID: {AssociationId} per record {RecordNumber}", associationId, recordNumber);
|
Logger.LogDebug("COMPOSITE: Associazione creata con ID: {AssociationId} per record {RecordNumber} (PARALLEL)", associationId, recordNumber);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -2761,12 +2825,12 @@ public partial class DataCoupler : ComponentBase
|
|||||||
var sourceKey = GenerateSourceKey(originalRecord);
|
var sourceKey = GenerateSourceKey(originalRecord);
|
||||||
if (string.IsNullOrEmpty(sourceKey)) return;
|
if (string.IsNullOrEmpty(sourceKey)) return;
|
||||||
|
|
||||||
var existingAssociation = await CredentialService.FindKeyAssociationByValueAsync(
|
var existingAssociation = await CredentialService.FindKeyAssociationByValueParallelAsync(
|
||||||
sourceKey, selectedRestEntity?.Name ?? "", selectedRestCredential ?? "");
|
sourceKey, selectedRestEntity?.Name ?? "", selectedRestCredential ?? "");
|
||||||
|
|
||||||
if (existingAssociation != null)
|
if (existingAssociation != null)
|
||||||
{
|
{
|
||||||
await CredentialService.DeleteKeyAssociationAsync(existingAssociation.Id);
|
await CredentialService.DeleteKeyAssociationParallelAsync(existingAssociation.Id);
|
||||||
Logger.LogInformation("COMPOSITE: Associazione non valida eliminata per record {RecordNumber}", recordNumber);
|
Logger.LogInformation("COMPOSITE: Associazione non valida eliminata per record {RecordNumber}", recordNumber);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@page "/"
|
@* @page "/"
|
||||||
|
|
||||||
<PageTitle>Index</PageTitle>
|
<PageTitle>Index</PageTitle>
|
||||||
|
|
||||||
@@ -6,4 +6,4 @@
|
|||||||
|
|
||||||
Welcome to your new app.
|
Welcome to your new app.
|
||||||
|
|
||||||
<SurveyPrompt Title="How is Blazor working for you?" />
|
<SurveyPrompt Title="How is Blazor working for you?" /> *@
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
|
<div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu">
|
||||||
<nav class="flex-column">
|
<nav class="flex-column">
|
||||||
<div class="nav-item px-3">
|
<div class="nav-item px-3">
|
||||||
|
@*
|
||||||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||||
<span class="oi oi-home" aria-hidden="true"></span> Home
|
<span class="oi oi-home" aria-hidden="true"></span> Home
|
||||||
</NavLink>
|
</NavLink>
|
||||||
@@ -23,20 +24,20 @@
|
|||||||
<NavLink class="nav-link" href="fetchdata">
|
<NavLink class="nav-link" href="fetchdata">
|
||||||
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
|
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
</div> *@
|
||||||
|
@* <div class="nav-item"></div> *@
|
||||||
|
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
|
||||||
|
<span class="oi oi-transfer" aria-hidden="true"></span> Data Coupler
|
||||||
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item px-3">
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="credentials">
|
<NavLink class="nav-link" href="credentials">
|
||||||
<span class="oi oi-key" aria-hidden="true"></span> Gestione Credenziali
|
<span class="oi oi-key" aria-hidden="true"></span> Gestione Credenziali
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item px-3">
|
|
||||||
<NavLink class="nav-link" href="data-coupler">
|
|
||||||
<span class="oi oi-transfer" aria-hidden="true"></span> Data Coupler
|
|
||||||
</NavLink>
|
|
||||||
</div>
|
|
||||||
<div class="nav-item px-3">
|
<div class="nav-item px-3">
|
||||||
<NavLink class="nav-link" href="key-associations">
|
<NavLink class="nav-link" href="key-associations">
|
||||||
<span class="oi oi-link-intact" aria-hidden="true"></span> Gestione Associazioni Chiave
|
<span class="oi oi-link-intact" aria-hidden="true"></span> Associazioni Chiavi
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item px-3">
|
<div class="nav-item px-3">
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user