Fix: Risolto double-mapping negli External ID Relationships per Salesforce

- Implementata funzionalità completa External ID Relationships nell'interfaccia di mapping
- Corretto bug double-mapping: i campi sorgente usati per External ID non vengono più inclusi nei mapping normali
- Risolto errore MALFORMED_ID causato dall'invio duplicato di campi come proprietà dirette e nested objects
- Implementata logica corretta per relationship names: oggetti standard usano il nome diretto, custom objects usano suffisso __r
- Aggiunta UI a 3 colonne (Object, External ID Field, Source Field) per configurazione External ID Relationships
- Migrazione database per supporto External ID Relationships nei profili
- Aggiornato ProfileSaver.razor.cs per salvare/caricare External ID Relationships
- Aggiornato ScheduledProfileExecutionService.cs per gestire External ID nelle esecuzioni schedulate
- Formato JSON output corretto: { 'Account': { 'CardCode__c': 'V50000' } }

Documentazione: EXTERNAL_ID_RELATIONSHIPS_IMPLEMENTATION.md
This commit is contained in:
2026-02-15 18:44:15 +01:00
parent ed5316fbdf
commit 483eb7b407
16 changed files with 1923 additions and 21 deletions
+350
View File
@@ -0,0 +1,350 @@
# 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
### 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