- Fix FindEntitiesByKeysAsync: SOQL query universale al posto di External ID GET * Disabilitato External ID GET (funziona solo per campi marcati External ID) * Query SOQL come metodo primario funzionante per tutti i campi * Logging dettagliato per diagnostica ricerche Salesforce - Pre-Discovery nei trasferimenti manuali (DataCoupler.razor.cs) * Ricerca automatica nella destinazione quando KeyAssociation non esiste * Creazione associazione con marker CreatedBy="PreDiscovery" * Aggiornamento forzato senza controllo hash per associazioni Pre-Discovery * Supporto parallel processing thread-safe - Pre-Discovery nei trasferimenti schedulati (ScheduledProfileExecutionService.cs) * Logica identica a trasferimenti manuali * Marker aggiuntivo ScheduledTransfer=true per tracciabilità * Aggiornamento forzato per prima sincronizzazione - Sistema aggiornamento forzato * Verifica AdditionalInfo per identificare associazioni Pre-Discovery * Skip controllo hash per associazioni appena scoperte * Garantisce sincronizzazione dati al primo trasferimento Vantaggi: - Prevenzione automatica duplicati in Salesforce - Recupero record esistenti senza associazioni - Parità funzionale tra esecuzioni manuali e schedulate - Performance ottimizzate con controllo hash per esecuzioni successive Docs: PRE_DISCOVERY_SYSTEM.md, PRE_DISCOVERY_FORCED_UPDATE.md, SALESFORCE_FIND_ENTITIES_FIX.md
11 KiB
Sistema Pre-Discovery per Associazioni Esistenti
Data: 21 Ottobre 2025
🎯 Obiettivo
Implementato un sistema di pre-discovery automatico che, prima di creare nuovi record, verifica se esistono già record nella destinazione che corrispondono ai dati sorgente. Se trovati, crea automaticamente un'associazione e procede con l'aggiornamento invece della creazione.
📋 Funzionamento
Flusso Operativo
Quando l'utente clicca su "Avvia trasferimento dati", per ogni record sorgente il sistema esegue i seguenti controlli:
1. Verifica Associazione Locale
↓
2. Se NON esiste → PRE-DISCOVERY nella Destinazione
↓
3a. Trovato → Crea Associazione + Aggiorna Record
↓
3b. NON Trovato → Crea Nuovo Record + Associazione
Dettaglio Step
Step 1: Verifica Associazione Locale
// Cerca nella tabella KeyAssociations
var existingAssociation = await CredentialService.FindKeyAssociationByValueParallelAsync(
sourceKey, // Es: "C00001"
currentEntityName, // Es: "Account"
currentCredentialName // Es: "Salesforce_Prod"
);
Risultato possibile:
- ✅ Trovata: Procedi con aggiornamento del record esistente
- ❌ Non trovata: Passa allo Step 2
Step 2: Pre-Discovery nella Destinazione
Se non esiste associazione locale, cerca direttamente nella destinazione REST:
// Prepara ricerca usando il campo mappato
var searchFields = new Dictionary<string, object>
{
{ mappedDestinationFieldName, sourceKey }
// Es: { "cardcode__c", "C00001" }
};
// Cerca nella REST API
var existingEntities = await currentRestClient.FindEntitiesByKeysAsync(
currentEntityName, // "Account"
searchFields // { "cardcode__c": "C00001" }
);
Risultato possibile:
- ✅ Trovato record: Vai allo Step 3a
- ❌ Non trovato: Vai allo Step 3b
Step 3a: Creazione Associazione + Aggiornamento
Se il record esiste nella destinazione:
// 1. Crea associazione
var newAssociation = new KeyAssociation
{
KeyValue = sourceKey, // "C00001"
SourceKeyField = sourceKeyField, // "CardCode"
DestinationKeyField = "Id", // Campo ID standard
MappedDestinationField = mappedDestinationField,// "cardcode__c"
DestinationEntity = currentEntityName, // "Account"
DestinationId = destinationId, // "001xx000003DGb2AAG"
RestCredentialName = currentCredentialName, // "Salesforce_Prod"
CreatedAt = DateTime.UtcNow,
LastVerifiedAt = DateTime.UtcNow,
IsActive = true,
Data_Hash = currentDataHash, // Hash dei dati mappati
AdditionalInfo = JSON con metadata "PreDiscovery"
};
// 2. Salva associazione
var associationId = await CredentialService.SaveKeyAssociationParallelAsync(newAssociation);
// 3. Procedi con AGGIORNAMENTO del record esistente
Step 3b: Creazione Normale
Se il record NON esiste:
// 1. Crea nuovo record nella destinazione
var result = await restClient.CreateEntityAsync(entityName, data);
// 2. Crea associazione con il nuovo ID
var association = new KeyAssociation { ... };
await CredentialService.SaveKeyAssociationParallelAsync(association);
🔧 Implementazione Tecnica
File Modificati
Data_Coupler/Pages/DataCoupler.razor.cs- Metodo
StartDataTransferWithComposite()- linea ~2620 - Metodo
StartDataTransferOriginal()- linea ~1290
- Metodo
Codice Chiave
// 🔍 PRE-DISCOVERY: Se non esiste associazione, cerca nella destinazione
if (existingAssociation == null)
{
Logger.LogDebug("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))
{
try
{
// Prepara i campi di ricerca
var searchFields = new Dictionary<string, object>
{
{ mappedDestinationFieldName, sourceKey }
};
// Cerca nella destinazione REST
var existingEntities = await currentRestClient.FindEntitiesByKeysAsync(
currentEntityName, searchFields);
if (existingEntities != null && existingEntities.Count > 0)
{
var foundEntity = existingEntities[0];
var destinationId = foundEntity.ContainsKey("Id")
? foundEntity["Id"]?.ToString()
: foundEntity.ContainsKey("id")
? foundEntity["id"]?.ToString()
: null;
if (!string.IsNullOrEmpty(destinationId))
{
// Crea associazione e usa per aggiornamento
var newAssociation = new KeyAssociation { ... };
var associationId = await CredentialService.SaveKeyAssociationParallelAsync(newAssociation);
existingAssociation = newAssociation;
existingAssociation.Id = associationId;
}
}
}
catch (Exception discEx)
{
Logger.LogWarning(discEx, "PRE-DISCOVERY: Errore durante la ricerca nella destinazione per KeyValue: '{KeyValue}'", sourceKey);
}
}
}
📊 Esempio Pratico
Scenario: SAP Business One → Salesforce
Record Sorgente:
CardCode: "C00001"
CardName: "Acme Corp"
City: "Milano"
Mappings Configurati:
CardCode → cardcode__c
CardName → Name
City → BillingCity
Campo Chiave Selezionato: CardCode
Caso 1: Prima Esecuzione (Record Non Esiste)
1. Cerca associazione → ❌ Non trovata
2. PRE-DISCOVERY in Salesforce → ❌ Non trovato
3. CREA nuovo Account in Salesforce
4. Crea associazione: CardCode="C00001" → Id="001xx000003DGb2AAG"
Risultato:
- Account creato con ID
001xx000003DGb2AAG - Associazione salvata in KeyAssociations
Caso 2: Seconda Esecuzione (Record Già Esiste)
1. Cerca associazione → ✅ TROVATA!
2. AGGIORNA Account esistente (ID: 001xx000003DGb2AAG)
Risultato:
- Account aggiornato
- Nessun duplicato
Caso 3: Record Esiste ma Senza Associazione
Situazione: Il record è stato creato manualmente in Salesforce con cardcode__c = "C00001" ma non esiste associazione.
1. Cerca associazione → ❌ Non trovata
2. PRE-DISCOVERY in Salesforce → ✅ TROVATO! (ID: 001xx000003DGb2AAG)
3. CREA associazione automatica
4. AGGIORNA Account esistente (ID: 001xx000003DGb2AAG)
Risultato:
- Associazione creata automaticamente
- Account aggiornato invece di creare duplicato
- ✨ Sincronizzazione senza duplicati!
🎁 Vantaggi
1. Prevenzione Duplicati Automatica
- Il sistema trova record esistenti anche se creati manualmente
- Non richiede pre-caricamento delle associazioni
2. Sincronizzazione Intelligente
- Aggiorna invece di creare se il record esiste
- Mantiene la coerenza tra sistemi
3. Recupero Dati Legacy
- Se hai già dati in Salesforce, il sistema li "scopre" e crea le associazioni automaticamente
- Utile per migrazioni da sistemi esistenti
4. Performance Ottimizzate
- Ricerca parallela thread-safe
- Cache delle associazioni già trovate
5. Logging Dettagliato
- Traccia completa del processo di discovery
- Debug facilitato con log prefissati
PRE-DISCOVERY:
📋 Log di Esempio
Discovery Successful
[Debug] PRE-DISCOVERY: Nessuna associazione trovata per 'C00001'. Cerco nella destinazione...
[Debug] PRE-DISCOVERY: Cerco in 'Account' dove cardcode__c = 'C00001'
[Info] PRE-DISCOVERY: ✅ Trovato record esistente! KeyValue: 'C00001' -> DestinationId: '001xx000003DGb2AAG'
[Info] PRE-DISCOVERY: Associazione creata con ID: 42
[Info] COMPOSITE PARALLEL: Record 1 marcato per aggiornamento (EntityId: 001xx000003DGb2AAG)
Discovery Not Found
[Debug] PRE-DISCOVERY: Nessuna associazione trovata per 'C00001'. Cerco nella destinazione...
[Debug] PRE-DISCOVERY: Cerco in 'Account' dove cardcode__c = 'C00001'
[Debug] PRE-DISCOVERY: Nessun record esistente trovato per KeyValue: 'C00001'
[Debug] COMPOSITE PARALLEL: Record 1 marcato per creazione
Discovery Error
[Warning] PRE-DISCOVERY: Errore durante la ricerca nella destinazione per KeyValue: 'C00001'
System.Net.Http.HttpRequestException: Response status code does not indicate success: 400 (Bad Request)
[Debug] COMPOSITE PARALLEL: Record 1 marcato per creazione
⚙️ Configurazione
Requisiti
- Campo Chiave Sorgente: Deve essere selezionato e mappato
- Sistema Associazioni: Deve essere abilitato (
useRecordAssociations = true) - Mapping Valido: Il campo chiave deve essere presente nei field mappings
Quando Si Attiva
La pre-discovery si attiva automaticamente quando:
- ✅ Il sistema di associazioni è abilitato
- ✅ Non esiste associazione per il valore chiave
- ✅ Il campo chiave è mappato a un campo destinazione
- ✅ Il client REST supporta
FindEntitiesByKeysAsync
Quando NON Si Attiva
- ❌ Associazione già presente (non serve discovery)
- ❌ Campo chiave non mappato
- ❌ Sistema associazioni disabilitato
- ❌ Errore durante la ricerca (fallback a creazione normale)
🧪 Testing
Test Case 1: Record Già in Destinazione
Setup:
- Crea manualmente un Account in Salesforce con
cardcode__c = "TEST001" - Configura mapping:
CardCode → cardcode__c - Seleziona
CardCodecome campo chiave
Esecuzione:
- Trasferisci record sorgente con
CardCode = "TEST001"
Risultato Atteso:
- ✅ Pre-discovery trova il record
- ✅ Associazione creata automaticamente
- ✅ Record aggiornato (non creato duplicato)
Test Case 2: Record Non Esiste
Setup:
- Salesforce NON contiene Account con
cardcode__c = "NEW001"
Esecuzione:
- Trasferisci record sorgente con
CardCode = "NEW001"
Risultato Atteso:
- ❌ Pre-discovery non trova nulla
- ✅ Nuovo record creato
- ✅ Associazione creata con nuovo ID
Test Case 3: Più Trasferimenti
Setup:
- Esegui trasferimento 1 volta (crea record)
Esecuzione:
- Esegui trasferimento 2° volta con stessi dati
Risultato Atteso:
- ✅ Associazione trovata al primo controllo
- ✅ Record aggiornato senza pre-discovery
- ✅ Nessun duplicato creato
🔒 Sicurezza
Thread-Safety
- Usa
SaveKeyAssociationParallelAsyncper gestire race conditions - Le ricerche REST sono stateless e thread-safe
Gestione Errori
- Errori in pre-discovery non bloccano il trasferimento
- Fallback automatico a creazione normale
- Logging warning per diagnosi
Validazione Dati
- Verifica presenza ID valido prima di creare associazione
- Controlla compatibilità Entity e Credential
- Supporta varianti case-sensitive di "Id" field
📚 Riferimenti
Metodi Utilizzati
FindKeyAssociationByValueParallelAsync()FindEntitiesByKeysAsync()SaveKeyAssociationParallelAsync()UpdateEntityAsync()
Entità Coinvolte
KeyAssociation- Tabella associazionifieldMappings- Dictionary mapping sorgente→destinazioneIRestServiceClient- Interfaccia REST per ricerca
✅ Status: IMPLEMENTATO
Il sistema è completamente funzionale e attivo in entrambi i metodi di trasferimento:
- ✅
StartDataTransferWithComposite()(Salesforce Composite API) - ✅
StartDataTransferOriginal()(REST API standard)
Versione: 1.0
Data Implementazione: 21 Ottobre 2025
Autore: Sistema Data Coupler