refactor: Centralizzata logica Pre-Discovery in servizio dedicato

Creato AssociationService per eliminare duplicazione codice e migliorare manutenibilità

Nuovo servizio:
- Data_Coupler/Services/AssociationService.cs (276 righe)
  * Interfaccia IAssociationService con metodi pubblici
  * PreDiscoveryRequest DTO per parametri configurabili
  * FindOrCreateAssociationAsync(): ricerca locale + Pre-Discovery REST
  * IsPreDiscoveryAssociation(): verifica marker associazioni Pre-Discovery

Refactoring DataCoupler.razor.cs:
- Injected IAssociationService nel componente
- StartDataTransferOriginal(): ridotto da 98 a 20 righe (-78)
- StartDataTransferWithComposite(): ridotto da 93 a 20 righe (-73)
- Verifica Pre-Discovery: ridotto da 20 a 2 righe (-18)
- Sostituito logica inline con chiamate al servizio centralizzato

Refactoring ScheduledProfileExecutionService.cs:
- Injected IAssociationService nel costruttore
- ExecuteDataTransferWithCompositeAsync(): ridotto da 99 a 20 righe (-79)
- Verifica Pre-Discovery: ridotto da 20 a 2 righe (-18)
- Parametro IsScheduledTransfer=true per tracciabilità

Dependency Injection:
- Registrato IAssociationService in Program.cs come Scoped
- Disponibile per dependency injection in tutti i componenti

Vantaggi:
- Eliminata duplicazione: 3 implementazioni → 1 servizio centralizzato
- Codice ridotto di 266 righe (330 → 64 nelle chiamate)
- Manutenibilità: modifiche future in un solo file
- Testabilità: interfaccia facilmente mockabile per unit test
- Riusabilità: servizio disponibile per futuri componenti
- Separazione responsabilità: logica associazioni isolata

Comportamento invariato:
- Nessuna modifica alla logica Pre-Discovery esistente
- Compatibilità completa con database e API
- Stessi marker e metadata nelle associazioni create

Docs: PRE_DISCOVERY_REFACTORING.md
Build:  Successo (0 errori, 25 warning pre-esistenti)
This commit is contained in:
2025-10-21 00:56:01 +02:00
parent 39d7124ce1
commit f513251507
5 changed files with 834 additions and 305 deletions
+35 -198
View File
@@ -26,6 +26,7 @@ public partial class DataCoupler : ComponentBase
[Inject] public IJSRuntime JSRuntime { get; set; } = default!;
[Inject] public ILogger<DataCoupler> Logger { get; set; } = default!;
[Inject] public IDataCouplerProfileService ProfileService { get; set; } = default!;
[Inject] public IAssociationService AssociationService { get; set; } = default!;
@@ -1312,102 +1313,26 @@ public partial class DataCoupler : ComponentBase
}
}
// 🔍 PRE-DISCOVERY: Se non esiste associazione, cerca nella destinazione
// 🔍 PRE-DISCOVERY: Usa il servizio centralizzato
if (existingAssociation == null)
{
Logger.LogInformation("PRE-DISCOVERY: Nessuna associazione trovata per '{KeyValue}'. Cerco nella destinazione...", sourceKey);
// Cerca il campo destinazione mappato al campo chiave sorgente
if (fieldMappings.TryGetValue(sourceKeyField, out var mappedDestinationFieldName))
var preDiscoveryRequest = new PreDiscoveryRequest
{
try
{
// Prepara i campi di ricerca: usa il campo mappato + il valore della chiave
var searchFields = new Dictionary<string, object>
{
{ mappedDestinationFieldName, sourceKey }
};
SourceKey = sourceKey,
SourceKeyField = sourceKeyField,
DestinationEntity = selectedRestEntity?.Name ?? "",
CredentialName = selectedRestCredential,
DestinationKeyField = GetEntityIdField(),
FieldMappings = fieldMappings,
RestClient = currentRestClient,
CurrentDataHash = null, // Non serve per metodo original
EnablePreDiscovery = true,
UseParallelMethod = false,
IsScheduledTransfer = false,
SourceType = selectedSourceType
};
Logger.LogInformation("PRE-DISCOVERY: Cerco in '{Entity}' dove {Field} = '{Value}'",
selectedRestEntity.Name, mappedDestinationFieldName, sourceKey);
// Cerca nella destinazione REST
var existingEntities = await currentRestClient.FindEntitiesByKeysAsync(
selectedRestEntity.Name, searchFields);
Logger.LogInformation("PRE-DISCOVERY: Risultati ricerca: {Count} entità trovate", existingEntities?.Count ?? 0);
if (existingEntities != null && existingEntities.Count > 0)
{
// Trovato! Prendi il primo risultato
var foundEntity = existingEntities[0];
Logger.LogInformation("PRE-DISCOVERY: Campi entità trovata: {Fields}",
string.Join(", ", foundEntity.Keys));
// Estrai l'ID del record trovato
var destinationId = foundEntity.ContainsKey("Id")
? foundEntity["Id"]?.ToString()
: foundEntity.ContainsKey("id")
? foundEntity["id"]?.ToString()
: null;
if (!string.IsNullOrEmpty(destinationId))
{
Logger.LogInformation("PRE-DISCOVERY: ✅ Trovato record esistente! KeyValue: '{KeyValue}' -> DestinationId: '{DestinationId}'",
sourceKey, destinationId);
// Crea l'associazione prima di procedere
var destinationKeyField = GetEntityIdField();
var newAssociation = new CredentialManager.Models.KeyAssociation
{
KeyValue = sourceKey,
SourceKeyField = sourceKeyField,
DestinationKeyField = destinationKeyField,
MappedDestinationField = mappedDestinationFieldName,
DestinationEntity = selectedRestEntity.Name,
DestinationId = destinationId,
RestCredentialName = selectedRestCredential,
CreatedAt = DateTime.UtcNow,
LastVerifiedAt = DateTime.UtcNow,
IsActive = true,
AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new
{
CreatedBy = "PreDiscovery",
DiscoveredAt = DateTime.UtcNow,
MappingCount = fieldMappings.Count,
SourceType = selectedSourceType
})
};
// Salva l'associazione
var associationId = await CredentialService.SaveKeyAssociationAsync(newAssociation);
Logger.LogInformation("PRE-DISCOVERY: Associazione creata con ID: {AssociationId}", associationId);
// Usa l'associazione appena creata per il resto del flusso
existingAssociation = newAssociation;
existingAssociation.Id = associationId;
}
else
{
Logger.LogWarning("PRE-DISCOVERY: Record trovato ma senza ID valido per KeyValue: '{KeyValue}'", sourceKey);
}
}
else
{
Logger.LogInformation("PRE-DISCOVERY: Nessun record esistente trovato per KeyValue: '{KeyValue}'", sourceKey);
}
}
catch (Exception discEx)
{
Logger.LogWarning(discEx, "PRE-DISCOVERY: Errore durante la ricerca nella destinazione per KeyValue: '{KeyValue}'", sourceKey);
// Continua comunque, il record verrà creato normalmente
}
}
else
{
Logger.LogWarning("PRE-DISCOVERY: Campo chiave '{SourceKeyField}' non trovato nei mappings. Skip discovery.", sourceKeyField);
}
existingAssociation = await AssociationService.FindOrCreateAssociationAsync(preDiscoveryRequest);
}
Logger.LogInformation("ASSOCIATION DEBUG: Associazione finale: {Found}. ID: {AssociationId}, DestinationId: '{DestinationId}', IsActive: {IsActive}",
@@ -2743,121 +2668,33 @@ public partial class DataCoupler : ComponentBase
}
}
// 🔍 PRE-DISCOVERY: Se non esiste associazione, cerca nella destinazione
// 🔍 PRE-DISCOVERY: Usa il servizio centralizzato
if (existingAssociation == null)
{
Logger.LogInformation("PRE-DISCOVERY: Nessuna associazione trovata per '{KeyValue}'. Cerco nella destinazione...", sourceKey);
// Cerca il campo destinazione mappato al campo chiave sorgente
if (currentFieldMappings.TryGetValue(currentSourceKeyField, out var mappedDestinationFieldName))
var preDiscoveryRequest = new PreDiscoveryRequest
{
try
{
// Prepara i campi di ricerca: usa il campo mappato + il valore della chiave
var searchFields = new Dictionary<string, object>
{
{ mappedDestinationFieldName, sourceKey }
};
SourceKey = sourceKey,
SourceKeyField = currentSourceKeyField,
DestinationEntity = currentEntityName,
CredentialName = currentCredentialName,
DestinationKeyField = GetEntityIdField(),
FieldMappings = currentFieldMappings,
RestClient = currentRestClient,
CurrentDataHash = currentDataHash,
EnablePreDiscovery = true,
UseParallelMethod = true, // Usa metodi paralleli thread-safe
IsScheduledTransfer = false
};
Logger.LogInformation("PRE-DISCOVERY: Cerco in '{Entity}' dove {Field} = '{Value}'",
currentEntityName, mappedDestinationFieldName, sourceKey);
// Cerca nella destinazione REST
var existingEntities = await currentRestClient.FindEntitiesByKeysAsync(
currentEntityName, searchFields);
if (existingEntities != null && existingEntities.Count > 0)
{
// Trovato! Prendi il primo risultato
var foundEntity = existingEntities[0];
// Estrai l'ID del record trovato
var destinationId = foundEntity.ContainsKey("Id")
? foundEntity["Id"]?.ToString()
: foundEntity.ContainsKey("id")
? foundEntity["id"]?.ToString()
: null;
if (!string.IsNullOrEmpty(destinationId))
{
Logger.LogInformation("PRE-DISCOVERY: ✅ Trovato record esistente! KeyValue: '{KeyValue}' -> DestinationId: '{DestinationId}'",
sourceKey, destinationId);
// Crea l'associazione prima di procedere
var destinationKeyField = GetEntityIdField();
var newAssociation = new CredentialManager.Models.KeyAssociation
{
KeyValue = sourceKey,
SourceKeyField = currentSourceKeyField,
DestinationKeyField = destinationKeyField,
MappedDestinationField = mappedDestinationFieldName,
DestinationEntity = currentEntityName,
DestinationId = destinationId,
RestCredentialName = currentCredentialName,
CreatedAt = DateTime.UtcNow,
LastVerifiedAt = DateTime.UtcNow,
IsActive = true,
Data_Hash = currentDataHash,
AdditionalInfo = System.Text.Json.JsonSerializer.Serialize(new
{
CreatedBy = "PreDiscovery",
DiscoveredAt = DateTime.UtcNow,
MappingCount = currentFieldMappings.Count
})
};
// Salva l'associazione (metodo parallelo thread-safe)
var associationId = await CredentialService.SaveKeyAssociationParallelAsync(newAssociation);
Logger.LogInformation("PRE-DISCOVERY: Associazione creata con ID: {AssociationId}", associationId);
// Usa l'associazione appena creata per il resto del flusso
existingAssociation = newAssociation;
existingAssociation.Id = associationId;
}
else
{
Logger.LogWarning("PRE-DISCOVERY: Record trovato ma senza ID valido per KeyValue: '{KeyValue}'", sourceKey);
}
}
else
{
Logger.LogInformation("PRE-DISCOVERY: Nessun record esistente trovato per KeyValue: '{KeyValue}'", sourceKey);
}
}
catch (Exception discEx)
{
Logger.LogWarning(discEx, "PRE-DISCOVERY: Errore durante la ricerca nella destinazione per KeyValue: '{KeyValue}'", sourceKey);
// Continua comunque, il record verrà creato normalmente
}
}
else
{
Logger.LogWarning("PRE-DISCOVERY: Campo chiave '{SourceKeyField}' non trovato nei mappings. Skip discovery.", currentSourceKeyField);
}
existingAssociation = await AssociationService.FindOrCreateAssociationAsync(preDiscoveryRequest);
}
if (existingAssociation != null && existingAssociation.IsActive)
{
// Verifica se l'associazione è stata creata dal Pre-Discovery
var isPreDiscoveryAssociation = false;
if (!string.IsNullOrEmpty(existingAssociation.AdditionalInfo))
{
try
{
var additionalInfo = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(existingAssociation.AdditionalInfo);
if (additionalInfo != null && additionalInfo.ContainsKey("CreatedBy"))
{
var createdBy = additionalInfo["CreatedBy"]?.ToString();
isPreDiscoveryAssociation = createdBy == "PreDiscovery";
}
}
catch
{
// Ignora errori di parsing
}
}
// 🔍 PRE-DISCOVERY: Usa il servizio per verificare se è un'associazione Pre-Discovery
var isPreDiscoveryAssociation = AssociationService.IsPreDiscoveryAssociation(existingAssociation);
// 🔍 PRE-DISCOVERY: Se l'associazione è stata appena creata dal Pre-Discovery, FORZA l'aggiornamento
// Se l'associazione è stata appena creata dal Pre-Discovery, FORZA l'aggiornamento
if (isPreDiscoveryAssociation)
{
// Forza aggiornamento senza controllo hash