335d587c89
- 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
453 lines
17 KiB
Markdown
453 lines
17 KiB
Markdown
# 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<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 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<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:**
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
[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 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
|
|
|
|
### ⚠️ 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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"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:**
|
|
```json
|
|
{
|
|
"quoteName": "Quote A",
|
|
"territoryCode": "NORTH-WEST"
|
|
}
|
|
```
|
|
|
|
**Record Trasformato:**
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```csharp
|
|
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
|
|
|
|
1. **Nessun ID Salesforce Richiesto**: Non serve conoscere l'ID Salesforce dell'Account
|
|
2. **Lookup Automatico**: Salesforce cerca automaticamente l'oggetto correlato tramite External ID
|
|
3. **Upsert Intelligente**: Se non trova l'oggetto, può crearlo automaticamente (se configurato)
|
|
4. **Manutenzione Semplificata**: I codici esterni sono più stabili degli ID interni
|
|
5. **Normalizzazione Automatica**: Il sistema corregge automaticamente i nomi quando carica profili salvati
|
|
|
|
## 🔄 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
|
|
|
|
5. **⭐ 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 `__r` per tutte le relazioni
|
|
- **Benefici**: Profili esistenti vengono corretti automaticamente senza intervento manuale
|
|
|
|
### 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: "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__c` e correlato è `Account` → usa `Account__r` (non `Account`)
|
|
- **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](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 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
|