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:
@@ -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
|
||||
Reference in New Issue
Block a user