# 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) ```csharp 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) ```csharp [MaxLength(4000)] public string? ExternalIdRelationshipsJson { get; set; } ``` ### 2. Serializzazione/Deserializzazione #### **DataCouplerProfileService** (CredentialManager/Services/DataCouplerProfileService.cs) **Metodi Aggiunti:** - `SerializeExternalIdRelationships()` - Serializza lista DTO → JSON - `DeserializeExternalIdRelationships()` - 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:** 1. **Selezione Oggetto Correlato**: Dropdown con tutti gli oggetti REST disponibili 2. **Selezione External ID Field**: Dropdown con campi filtrati (terminanti con `__c`, `Id`, contengono "External") 3. **Selezione Campo Sorgente**: Dropdown con campi disponibili dalla sorgente dati 4. **Pulsante Aggiungi**: Conferma e aggiunge relazione alla lista 5. **Tabella Relazioni**: Visualizza tutte le relazioni configurate con formato di esempio **Visibilità Condizionale:** ```csharp @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:** ```csharp private List externalIdRelationships = new(); private string selectedRelationshipObject = string.Empty; private string selectedExternalIdField = string.Empty; private string selectedRelationshipSourceField = string.Empty; private List availableRelationshipObjects = new(); ``` **Metodi Implementati:** - `OnRelationshipObjectSelected()` - Gestisce selezione oggetto - `AddExternalIdRelationship()` - Aggiunge nuova relazione con validazione - `RemoveExternalIdRelationship()` - Rimuove relazione esistente - `GetExternalIdFieldsForSelectedObject()` - Ottiene campi External ID disponibili - `GetSourceFieldsForRelationship()` - 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() ```csharp // 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 { { relationship.ExternalIdField, transformedValue } }; restData[relationship.RelationshipName] = externalIdObject; } } } } ``` #### **ScheduledProfileExecutionService** - TransformRecordForRest() **Modifiche:** - Aggiunto parametro opzionale `List? 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:** ```csharp private List ParseExternalIdRelationships(string? externalIdRelationshipsJson) { // Deserializza JSON con stesse opzioni di DataCouplerProfileService var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; return JsonSerializer.Deserialize>(externalIdRelationshipsJson, options); } ``` ### 5. Salvataggio Profili #### **Components/ProfileSaver.razor.cs** **Modifiche:** - Aggiunto parametro `ExternalIdRelationships` - Incluso nella creazione del DTO per salvataggio profili ```csharp [Parameter] public List ExternalIdRelationships { get; set; } = new(); // In SaveProfile() ExternalIdRelationships = this.ExternalIdRelationships, ``` ### 6. Discovery REST API #### **Data_Coupler/Extensions/DataCoupler/RESTMethod.cs** **Modifiche:** - Aggiornato `ConnectToRestApi()` per popolare `availableRelationshipObjects` - Chiamata a `DiscoverEntitiesAsync()` per ottenere dettagli completi oggetti REST ```csharp 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:** 1. **20260203000000_AddExternalIdRelationships.cs** - Migrazione Entity Framework per aggiungere campo `ExternalIdRelationshipsJson` - Tipo: TEXT, MaxLength: 4000, Nullable 2. **20260203000000_AddExternalIdRelationships.sql** - Script SQL manuale per applicazione diretta se necessario - Include update di `__EFMigrationsHistory` ```sql ALTER TABLE DataCouplerProfiles ADD COLUMN ExternalIdRelationshipsJson TEXT; INSERT INTO __EFMigrationsHistory (MigrationId, ProductVersion) VALUES ('20260203000000_AddExternalIdRelationships', '9.0.0'); ``` ## 📊 Formato Dati Salesforce ### Esempio di Trasformazione **Configurazione:** - **Relationship Name**: `Account__r` - **Related Object**: `Account` - **External ID Field**: `Country__c` - **Source Field**: `CountryCode` (dalla tabella sorgente) **Record Sorgente:** ```json { "ProductName": "Widget A", "Price": 99.99, "CountryCode": "US" } ``` **Record Trasformato per Salesforce:** ```json { "Name": "Widget A", "Price__c": 99.99, "Account__r": { "Country__c": "US" } } ``` ### Vantaggi External ID 1. **Nessun ID Salesforce Richiesto**: Non serve conoscere l'ID Salesforce dell'Account 2. **Lookup Automatico**: Salesforce cerca automaticamente l'Account con `Country__c = "US"` 3. **Upsert Intelligente**: Se non trova l'Account, può crearlo automaticamente (se configurato) 4. **Manutenzione Semplificata**: I codici esterni sono più stabili degli ID interni ## 🔄 Flusso Operativo ### Configurazione Manuale (DataCoupler.razor) 1. Utente configura connessione sorgente (database/file) e destinazione (Salesforce) 2. Sistema scopre automaticamente oggetti REST disponibili 3. Utente configura field mappings principali 4. Sezione External ID Relationships diventa visibile 5. Utente seleziona: - Oggetto correlato (es: Account) - Campo External ID (es: Country__c) - Campo sorgente (es: CountryCode) 6. Click su "Aggiungi Relazione" → validazione e aggiunta alla lista 7. (Opzionale) Salvataggio come profilo per riutilizzo futuro 8. Esecuzione trasferimento → relazioni applicate automaticamente ### Esecuzione Schedulata (ScheduledProfileExecutionService) 1. Background service carica profilo dal database 2. Deserializza External ID Relationships da JSON 3. Estrae dati dalla sorgente 4. Trasforma ogni record applicando field mappings + External ID Relationships 5. Invia a Salesforce (Standard API o Composite API) 6. Gestisce associazioni record e hash per evitare duplicati ## 🧪 Testing ### Scenari di Test Consigliati 1. **Configurazione UI** - ✅ Selezione oggetti e campi funziona correttamente - ✅ Validazione impedisce relazioni incomplete - ✅ Aggiunta e rimozione relazioni aggiorna UI 2. **Salvataggio/Caricamento Profili** - ✅ Relazioni salvate correttamente in JSON - ✅ Profilo ricaricato ripristina tutte le relazioni - ✅ Database persiste ExternalIdRelationshipsJson 3. **Trasformazione Dati** - ✅ Record trasformato include dizionario annidato per relazioni - ✅ Valori null/vuoti gestiti correttamente - ✅ Logging dettagliato per ogni relazione aggiunta 4. **Esecuzione Schedulata** - ✅ Schedulazione carica e applica relazioni - ✅ Funziona sia con Standard API che Composite API - ✅ Errori gestiti e loggati senza bloccare il flusso 5. **Integrazione Salesforce** - ✅ Salesforce accetta formato External ID Relationship - ✅ Lookup automatico funziona correttamente - ✅ Record creati con relazioni corrette ## 📝 Note Implementative ### Decisioni di Design 1. **MaxLength JSON: 4000 caratteri** - Ragionamento: Supporta configurazioni complesse senza eccedere limiti SQLite - Alternativa: Se necessario più spazio, può essere aumentato a TEXT illimitato 2. **Parametro Opzionale in TransformRecordForRest** - Backward compatibility garantita - Chiamate esistenti senza External ID continuano a funzionare 3. **Filtro Campi External ID** - Logica: `EndsWith("__c") || Name == "Id" || Contains("External")` - Copre la maggior parte dei casi comuni in Salesforce - Personalizzabile se necessario 4. **Visibilità Condizionale UI** - Solo per Salesforce (verifica `IsSalesforceClient()`) - Solo dopo field mappings configurati (`fieldMappings.Any()`) - Migliora UX evitando confusione per altre API ### Potenziali Estensioni Future 1. **Validazione Avanzata**: Verifica esistenza oggetto/campo su Salesforce prima di salvare 2. **Multi-Level Relationships**: Supporto per relazioni annidate (es: `Account__r.Owner__r.Name__c`) 3. **Relazioni Composite**: Più External ID per stesso oggetto (es: FirstName + LastName) 4. **Import/Export Relazioni**: Backup e restore separato delle configurazioni relazioni 5. **Template Relazioni**: Libreria di relazioni predefinite per oggetti Salesforce comuni ## 🐛 Troubleshooting ### Errori Comuni **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](https://help.salesforce.com/s/articleView?id=sf.fields_about_custom_external_id.htm) - [Salesforce REST API - Insert or Update](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_upsert.htm) - [Salesforce Relationship Fields](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_relationship_fields.htm) --- **Implementazione Completata**: 3 Febbraio 2026 **Framework**: .NET 9.0 **Pattern**: Repository + DTO + Service Layer **Database**: SQLite con Entity Framework Core **UI**: Blazor Server con Bootstrap 5