- Salesforce Composite Batch API per describe SObject: le describe sono ora raggruppate in chunk da 25 e inviate come singole POST a /composite/batch, riducendo le chiamate API da N a ceil(N/25); per 200 SObject: da 201 a 9 chiamate. - Discovery entita' REST in parallelo: DiscoverEntitySummariesAsync e DiscoverEntitiesAsync avviate simultaneamente; la lista entita' diventa interattiva subito dopo le summaries, i dettagli completano in background con StateHasChanged() per aggiornare l'UI istantaneamente. - Fix scheduler - preservazione ExternalIdRelationshipsJson e DefaultValuesJson: in DataCoupler.razor.cs entrambi i blocchi di update profilo esistente (riattivazione profilo inattivo e sovrascrittura profilo attivo) omettevano questi campi nella copia, causandone l'azzeramento silenzioso ad ogni re-salvataggio. Ora entrambi i percorsi propagano correttamente i campi JSON. - Fix scheduler - esclusione campi sorgente External ID dal mapping normale: in ScheduledProfileExecutionService.TransformRecordForRest i campi sorgente usati nelle External ID Relationships venivano inclusi anche nel loop di field mapping standard, generando dati duplicati nell'entita' destinazione. Ora il comportamento e' allineato alla UI manuale (TransformRecordToRestEntity). - Aggiornata documentazione: README.md, AGENTS.md, copilot-instructions.md
17 KiB
Implementazione External ID Relationships per Salesforce
📋 Panoramica
Implementata la funzionalità completa per gestire External ID Relationships nell'interfaccia di mapping dei campi di Data-Coupler. Questa feature permette di creare relazioni tra oggetti Salesforce utilizzando External ID durante il trasferimento dati, evitando la necessità di conoscere gli ID Salesforce interni.
🎯 Obiettivi Raggiunti
- ✅ Estensione modelli dati (DTO ed Entity) per supportare External ID Relationships
- ✅ UI completa per configurazione relazioni con autocomplete
- ✅ Logica di trasformazione dati integrata in DataCoupler e ScheduledProfileExecutionService
- ✅ Supporto per salvataggio e caricamento relazioni in profili Data Coupler
- ✅ Migrazione database per persistenza configurazioni
- ✅ Supporto per esecuzioni schedulate
🏗️ Architettura Implementata
1. Modelli Dati
ExternalIdRelationshipDto (CredentialManager/Models/DataCouplerProfileDto.cs)
public class ExternalIdRelationshipDto
{
public string RelationshipName { get; set; } = string.Empty; // Es: "Account__r"
public string RelatedObjectName { get; set; } = string.Empty; // Es: "Account"
public string ExternalIdField { get; set; } = string.Empty; // Es: "Country__c"
public string SourceField { get; set; } = string.Empty; // Campo sorgente con valore
}
DataCouplerProfile Entity (CredentialManager/Models/DataCouplerProfile.cs)
[MaxLength(4000)]
public string? ExternalIdRelationshipsJson { get; set; }
2. Serializzazione/Deserializzazione
DataCouplerProfileService (CredentialManager/Services/DataCouplerProfileService.cs)
Metodi Aggiunti:
SerializeExternalIdRelationships()- Serializza lista DTO → JSONDeserializeExternalIdRelationships()- Deserializza JSON → lista DTO- Aggiornato
ToDto()per includere External ID Relationships - Aggiornato
FromDto()per serializzare relazioni - Aggiornato
UpdateProfileAsync()per persistere ExternalIdRelationshipsJson
3. Interfaccia Utente
DataCoupler.razor - Sezione External ID Relationships
Componenti UI:
- Selezione Oggetto Correlato: Dropdown con tutti gli oggetti REST disponibili
- Selezione External ID Field: Dropdown con campi filtrati (terminanti con
__c,Id, contengono "External") - Selezione Campo Sorgente: Dropdown con campi disponibili dalla sorgente dati
- Pulsante Aggiungi: Conferma e aggiunge relazione alla lista
- Tabella Relazioni: Visualizza tutte le relazioni configurate con formato di esempio
Visibilità Condizionale:
@if (fieldMappings.Any() && currentRestDiscovery != null && IsSalesforceClient())
- Mostrata solo per connessioni Salesforce
- Solo dopo aver configurato i field mappings principali
DataCoupler.razor.cs - Gestione Relazioni
Campi Aggiunti:
private List<ExternalIdRelationshipDto> externalIdRelationships = new();
private string selectedRelationshipObject = string.Empty;
private string selectedExternalIdField = string.Empty;
private string selectedRelationshipSourceField = string.Empty;
private List<RestEntityInfo> availableRelationshipObjects = new();
Metodi Implementati:
OnRelationshipObjectSelected()- Gestisce selezione oggettoAddExternalIdRelationship()- Aggiunge nuova relazione con validazioneRemoveExternalIdRelationship()- Rimuove relazione esistenteGetExternalIdFieldsForSelectedObject()- Ottiene campi External ID disponibiliGetSourceFieldsForRelationship()- Ottiene campi sorgente per mapping
Integrazione Reset/Clear:
- Aggiornato
ClearAllMappings()per pulire relazioni - Aggiornato
ResetAllState()per reset completo - Aggiornato
ApplyProfileConfiguration()per caricare relazioni da profilo
4. Trasformazione Dati
DataCoupler.razor.cs - TransformRecordToRestEntity()
// Aggiungi External ID Relationships (per Salesforce)
if (externalIdRelationships.Any())
{
foreach (var relationship in externalIdRelationships)
{
if (!string.IsNullOrWhiteSpace(relationship.SourceField) &&
dbRecord.ContainsKey(relationship.SourceField))
{
var sourceValue = dbRecord[relationship.SourceField];
var transformedValue = TransformValue(sourceValue, relationship.SourceField, relationship.ExternalIdField);
if (transformedValue != null)
{
// Formato: { "Account__r": { "Country__c": "US" } }
var externalIdObject = new Dictionary<string, object>
{
{ relationship.ExternalIdField, transformedValue }
};
restData[relationship.RelationshipName] = externalIdObject;
}
}
}
}
ScheduledProfileExecutionService - TransformRecordForRest()
Modifiche:
- Aggiunto parametro opzionale
List<ExternalIdRelationshipDto>? externalIdRelationships - Implementata stessa logica di trasformazione per esecuzioni schedulate
- Aggiornato
ExecuteDataTransferAsync()per deserializzare e passare relazioni - Aggiornato
ExecuteDataTransferStandardAsync()per accettare e usare relazioni - Aggiornato
ExecuteDataTransferWithCompositeAsync()per supporto Salesforce Composite API
Nuovo Metodo:
private List<ExternalIdRelationshipDto> ParseExternalIdRelationships(string? externalIdRelationshipsJson)
{
// Deserializza JSON con stesse opzioni di DataCouplerProfileService
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
return JsonSerializer.Deserialize<List<ExternalIdRelationshipDto>>(externalIdRelationshipsJson, options);
}
5. Salvataggio Profili
Components/ProfileSaver.razor.cs
Modifiche:
- Aggiunto parametro
ExternalIdRelationships - Incluso nella creazione del DTO per salvataggio profili
[Parameter]
public List<ExternalIdRelationshipDto> ExternalIdRelationships { get; set; } = new();
// In SaveProfile()
ExternalIdRelationships = this.ExternalIdRelationships,
6. Discovery REST API
Data_Coupler/Extensions/DataCoupler/RESTMethod.cs
Modifiche:
- Aggiornato
ConnectToRestApi()per popolareavailableRelationshipObjects - Chiamata a
DiscoverEntitiesAsync()per ottenere dettagli completi oggetti REST
try
{
availableRelationshipObjects = (await currentRestDiscovery.DiscoverEntitiesAsync()).ToList();
Logger.LogInformation("Caricati {Count} oggetti REST per External ID Relationships", availableRelationshipObjects.Count);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Impossibile caricare oggetti REST per External ID Relationships");
}
7. Migrazione Database
File Creati:
-
20260203000000_AddExternalIdRelationships.cs
- Migrazione Entity Framework per aggiungere campo
ExternalIdRelationshipsJson - Tipo: TEXT, MaxLength: 4000, Nullable
- Migrazione Entity Framework per aggiungere campo
-
20260203000000_AddExternalIdRelationships.sql
- Script SQL manuale per applicazione diretta se necessario
- Include update di
__EFMigrationsHistory
ALTER TABLE DataCouplerProfiles ADD COLUMN ExternalIdRelationshipsJson TEXT;
INSERT INTO __EFMigrationsHistory (MigrationId, ProductVersion)
VALUES ('20260203000000_AddExternalIdRelationships', '9.0.0');
📊 Formato Dati Salesforce
⚠️ REGOLA IMPORTANTE: Formato Basato sull'Oggetto DESTINAZIONE
Il formato delle External ID Relationships dipende dal tipo dell'oggetto DESTINAZIONE (quello che stai creando/aggiornando), NON dal tipo dell'oggetto correlato:
Se l'Oggetto DESTINAZIONE è CUSTOM (es. Sales_Quote__c, Custom_Order__c):
- ✅ Tutte le relazioni usano
__r, sia per oggetti standard che custom - Oggetto Standard:
"Account__r": { "External_ID__c": "value" } - Oggetto Custom:
"Custom_Company__r": { "External_ID__c": "value" }
Se l'Oggetto DESTINAZIONE è STANDARD (es. Opportunity, Contact):
- ✅ Solo oggetti custom correlati usano
__r - Oggetto Standard:
"Account": { "External_ID__c": "value" } - Oggetto Custom:
"Custom_Company__r": { "External_ID__c": "value" }
Esempi Pratici
Esempio 1: Destinazione CUSTOM → Relazione a Oggetto STANDARD
Scenario: Creo un record Sales_Quote__c collegato ad Account standard
Configurazione:
- Destination Object:
Sales_Quote__c(CUSTOM) - Relationship Name:
Account__r⚠️ Usa __r anche se Account è standard! - Related Object:
Account - External ID Field:
Codice_ERP__c - Source Field:
customerCode
Record Trasformato:
{
"Name": "Quote 2024-001",
"Quote_Code__c": "Q001",
"Account__r": {
"Codice_ERP__c": "C60000"
}
}
Esempio 2: Destinazione STANDARD → Relazione a Oggetto STANDARD
Scenario: Creo un record Opportunity collegato ad Account
Configurazione:
- Destination Object:
Opportunity(STANDARD) - Relationship Name:
Account⚠️ NON usa __r - Related Object:
Account - External ID Field:
Country__c - Source Field:
CountryCode
Record Trasformato:
{
"Name": "New Deal 2024",
"StageName": "Prospecting",
"Account": {
"Country__c": "US"
}
}
Esempio 3: Destinazione CUSTOM → Relazione a Oggetto CUSTOM
Configurazione:
- Destination Object:
Sales_Quote__c(CUSTOM) - Relationship Name:
Custom_Territory__r - Related Object:
Custom_Territory__c - External ID Field:
Territory_Code__c - Source Field:
territoryCode
Record Sorgente:
{
"quoteName": "Quote A",
"territoryCode": "NORTH-WEST"
}
Record Trasformato:
{
"Name": "Quote A",
"Custom_Territory__r": {
"Territory_Code__c": "NORTH-WEST"
}
}
Logica di Normalizzazione Automatica
Il sistema implementa il metodo NormalizeRelationshipName() che garantisce il formato corretto:
private string NormalizeRelationshipName(string relatedObjectName, bool isDestinationCustom)
{
if (isDestinationCustom)
{
// Destinazione CUSTOM: tutte le relazioni usano __r
if (relatedObjectName.EndsWith("__c"))
return relatedObjectName.Replace("__c", "__r"); // Custom_Obj__c → Custom_Obj__r
else
return relatedObjectName + "__r"; // Account → Account__r
}
else
{
// Destinazione STANDARD: solo oggetti custom usano __r
if (relatedObjectName.EndsWith("__c"))
return relatedObjectName.Replace("__c", "__r"); // Custom_Obj__c → Custom_Obj__r
else
return relatedObjectName; // Account → Account (no suffix)
}
}
Vantaggi External ID
- Nessun ID Salesforce Richiesto: Non serve conoscere l'ID Salesforce dell'Account
- Lookup Automatico: Salesforce cerca automaticamente l'oggetto correlato tramite External ID
- Upsert Intelligente: Se non trova l'oggetto, può crearlo automaticamente (se configurato)
- Manutenzione Semplificata: I codici esterni sono più stabili degli ID interni
- Normalizzazione Automatica: Il sistema corregge automaticamente i nomi quando carica profili salvati
🔄 Flusso Operativo
Configurazione Manuale (DataCoupler.razor)
- Utente configura connessione sorgente (database/file) e destinazione (Salesforce)
- Sistema scopre automaticamente oggetti REST disponibili
- Utente configura field mappings principali
- Sezione External ID Relationships diventa visibile
- Utente seleziona:
- Oggetto correlato (es: Account)
- Campo External ID (es: Country__c)
- Campo sorgente (es: CountryCode)
- Click su "Aggiungi Relazione" → validazione e aggiunta alla lista
- (Opzionale) Salvataggio come profilo per riutilizzo futuro
- Esecuzione trasferimento → relazioni applicate automaticamente
Esecuzione Schedulata (ScheduledProfileExecutionService)
- Background service carica profilo dal database
- Deserializza External ID Relationships da JSON
- Estrae dati dalla sorgente
- Trasforma ogni record applicando field mappings + External ID Relationships
- Invia a Salesforce (Standard API o Composite API)
- Gestisce associazioni record e hash per evitare duplicati
🧪 Testing
Scenari di Test Consigliati
-
Configurazione UI
- ✅ Selezione oggetti e campi funziona correttamente
- ✅ Validazione impedisce relazioni incomplete
- ✅ Aggiunta e rimozione relazioni aggiorna UI
-
Salvataggio/Caricamento Profili
- ✅ Relazioni salvate correttamente in JSON
- ✅ Profilo ricaricato ripristina tutte le relazioni
- ✅ Database persiste ExternalIdRelationshipsJson
-
Trasformazione Dati
- ✅ Record trasformato include dizionario annidato per relazioni
- ✅ Valori null/vuoti gestiti correttamente
- ✅ Logging dettagliato per ogni relazione aggiunta
-
Esecuzione Schedulata
- ✅ Schedulazione carica e applica relazioni
- ✅ Funziona sia con Standard API che Composite API
- ✅ Errori gestiti e loggati senza bloccare il flusso
-
Integrazione Salesforce
- ✅ Salesforce accetta formato External ID Relationship
- ✅ Lookup automatico funziona correttamente
- ✅ Record creati con relazioni corrette
📝 Note Implementative
Decisioni di Design
-
MaxLength JSON: 4000 caratteri
- Ragionamento: Supporta configurazioni complesse senza eccedere limiti SQLite
- Alternativa: Se necessario più spazio, può essere aumentato a TEXT illimitato
-
Parametro Opzionale in TransformRecordForRest
- Backward compatibility garantita
- Chiamate esistenti senza External ID continuano a funzionare
-
Filtro Campi External ID
- Logica:
EndsWith("__c") || Name == "Id" || Contains("External") - Copre la maggior parte dei casi comuni in Salesforce
- Personalizzabile se necessario
- Logica:
-
Visibilità Condizionale UI
- Solo per Salesforce (verifica
IsSalesforceClient()) - Solo dopo field mappings configurati (
fieldMappings.Any()) - Migliora UX evitando confusione per altre API
- Solo per Salesforce (verifica
-
⭐ Normalizzazione Automatica RelationshipName (FIX CRITICO - 17 Feb 2026)
- Problema Risolto: Errore
"No such column 'Account' on sobject of type Sales_Quote__c" - Causa: Il formato dipende dall'oggetto DESTINAZIONE, non dall'oggetto correlato
- Soluzione: Metodo
NormalizeRelationshipName()controlla tipo oggetto destinazione - Funzionalità: Corregge automaticamente i RelationshipName al caricamento profili
- Regola: Se destinazione è custom → usa SEMPRE
__rper tutte le relazioni - Benefici: Profili esistenti vengono corretti automaticamente senza intervento manuale
- Problema Risolto: Errore
Potenziali Estensioni Future
- Validazione Avanzata: Verifica esistenza oggetto/campo su Salesforce prima di salvare
- Multi-Level Relationships: Supporto per relazioni annidate (es:
Account__r.Owner__r.Name__c) - Relazioni Composite: Più External ID per stesso oggetto (es: FirstName + LastName)
- Import/Export Relazioni: Backup e restore separato delle configurazioni relazioni
- Template Relazioni: Libreria di relazioni predefinite per oggetti Salesforce comuni
🐛 Troubleshooting
Errori Comuni
⚠️ Errore: "No such column 'Account' on sobject of type Sales_Quote__c"
- Causa: RelationshipName incorretto per oggetto destinazione custom
- Spiegazione: Quando l'oggetto DESTINAZIONE è custom (es.
Sales_Quote__c), TUTTE le relazioni devono usare__r, anche per oggetti standard - Soluzione AUTOMATICA: ✅ Il sistema ora normalizza automaticamente i nomi delle relazioni
- Esempio: Se destinazione è
Sales_Quote__ce correlato èAccount→ usaAccount__r(nonAccount) - Fix Manuale: Se usi profili vecchi, il sistema correggerà automaticamente al caricamento
Errore: "External ID field not found"
- Causa: Campo External ID non esiste sull'oggetto Salesforce
- Soluzione: Verificare che il campo sia configurato come External ID in Salesforce
Errore: "Multiple records found with external ID"
- Causa: External ID non è univoco in Salesforce
- Soluzione: Verificare unicità del campo External ID
Relazioni Non Applicate
- Causa:
externalIdRelationshipsè vuoto - Soluzione: Verificare deserializzazione JSON in profilo
UI Non Mostra Sezione Relazioni
- Causa: Condizione visibilità non soddisfatta
- Soluzione: Verificare che sia Salesforce e field mappings configurati
📚 Riferimenti
- Salesforce External ID Documentation
- Salesforce REST API - Insert or Update
- Salesforce Relationship Fields
Implementazione Iniziale: 3 Febbraio 2026
Ultimo Aggiornamento: 17 Febbraio 2026 - ⭐ FIX CRITICO: Normalizzazione automatica RelationshipName
Framework: .NET 9.0
Pattern: Repository + DTO + Service Layer
Database: SQLite con Entity Framework Core
UI: Blazor Server con Bootstrap 5