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
@@ -164,18 +164,25 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
throw new InvalidOperationException("Nessun mapping dei campi configurato per il profilo");
}
// 4.5. Parse External ID Relationships (Salesforce)
var externalIdRelationships = ParseExternalIdRelationships(profile.ExternalIdRelationshipsJson);
if (externalIdRelationships.Any())
{
_logger.LogInformation("Caricate {Count} External ID Relationships dal profilo", externalIdRelationships.Count);
}
// 5. Determina se utilizzare Salesforce Composite API
bool useSalesforceComposite = restClient is DataConnection.REST.Implementations.SalesforceServiceClient;
if (useSalesforceComposite)
{
_logger.LogInformation("Utilizzo Salesforce Composite API per il trasferimento");
return await ExecuteDataTransferWithCompositeAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, enableDeletionSync);
return await ExecuteDataTransferWithCompositeAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, externalIdRelationships, enableDeletionSync);
}
else
{
_logger.LogInformation("Utilizzo metodo trasferimento standard per il trasferimento");
return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, enableDeletionSync);
return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential!, fieldMappings, externalIdRelationships, enableDeletionSync);
}
}
catch (Exception ex)
@@ -363,6 +370,53 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
return mappings;
}
/// <summary>
/// Deserializza gli External ID Relationships dal JSON del profilo
/// </summary>
private List<ExternalIdRelationshipDto> ParseExternalIdRelationships(string? externalIdRelationshipsJson)
{
var relationships = new List<ExternalIdRelationshipDto>();
if (string.IsNullOrEmpty(externalIdRelationshipsJson))
{
_logger.LogDebug("ExternalIdRelationships JSON è vuoto o null");
return relationships;
}
_logger.LogDebug("Parsing ExternalIdRelationships JSON: {Json}", externalIdRelationshipsJson);
try
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
var relationshipsList = JsonSerializer.Deserialize<List<ExternalIdRelationshipDto>>(externalIdRelationshipsJson, options);
if (relationshipsList != null)
{
relationships = relationshipsList;
_logger.LogInformation("Trovati {Count} External ID Relationships nel JSON", relationships.Count);
foreach (var rel in relationships)
{
_logger.LogDebug("External ID Relationship: {RelationshipName} - {RelatedObject}.{ExternalIdField} <- {SourceField}",
rel.RelationshipName, rel.RelatedObjectName, rel.ExternalIdField, rel.SourceField);
}
}
else
{
_logger.LogWarning("Deserializzazione ritornato null per ExternalIdRelationships JSON");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nel parsing degli ExternalIdRelationships: {Json}", externalIdRelationshipsJson);
}
return relationships;
}
/// <summary>
/// Ottiene tutti i record dal database
/// </summary>
@@ -631,6 +685,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
RestEntitySummary restEntity,
RestApiCredential restCredential,
Dictionary<string, string> fieldMappings,
List<ExternalIdRelationshipDto> externalIdRelationships,
bool enableDeletionSync = false)
{
_logger.LogInformation("Iniziando trasferimento dati standard per {RecordCount} record - DeletionSync: {DeletionSync}",
@@ -644,8 +699,8 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
{
try
{
// 1. Trasforma il record utilizzando i field mappings
var restData = TransformRecordForRest(record, fieldMappings);
// 1. Trasforma il record utilizzando i field mappings e External ID Relationships
var restData = TransformRecordForRest(record, fieldMappings, externalIdRelationships);
// 2. Gestione associazioni record se abilitata
string? entityId = null;
@@ -755,6 +810,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
RestEntitySummary restEntity,
RestApiCredential restCredential,
Dictionary<string, string> fieldMappings,
List<ExternalIdRelationshipDto> externalIdRelationships,
bool enableDeletionSync = false)
{
_logger.LogInformation("Iniziando trasferimento dati COMPOSITE per {RecordCount} record - DeletionSync: {DeletionSync}",
@@ -764,7 +820,7 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
if (!(restClient is DataConnection.REST.Implementations.SalesforceServiceClient salesforceClient))
{
_logger.LogWarning("Client REST non è SalesforceServiceClient, fallback al metodo standard");
return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential, fieldMappings, enableDeletionSync);
return await ExecuteDataTransferStandardAsync(profile, sourceRecords, restClient, restEntity, restCredential, fieldMappings, externalIdRelationships, enableDeletionSync);
}
try
@@ -794,8 +850,8 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
var record = indexedRecord.Record;
var recordNumber = indexedRecord.RecordNumber;
// Trasforma il record in base ai mapping (operazione locale, thread-safe)
var restData = TransformRecordForRest(record, fieldMappings);
// Trasforma il record in base ai mapping e External ID Relationships (operazione locale, thread-safe)
var restData = TransformRecordForRest(record, fieldMappings, externalIdRelationships);
// Genera la chiave sorgente e l'hash dei dati per questo record (include MAPPING_SIGNATURE)
var sourceKey = GenerateSourceKey(record, profile.SourceKeyField);
@@ -1085,7 +1141,10 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
/// <summary>
/// Trasforma un record sorgente in formato REST utilizzando i field mappings
/// </summary>
private Dictionary<string, object> TransformRecordForRest(Dictionary<string, object> sourceRecord, Dictionary<string, string> fieldMappings)
private Dictionary<string, object> TransformRecordForRest(
Dictionary<string, object> sourceRecord,
Dictionary<string, string> fieldMappings,
List<ExternalIdRelationshipDto>? externalIdRelationships = null)
{
var restData = new Dictionary<string, object>();
@@ -1105,6 +1164,35 @@ public class ScheduledProfileExecutionService : IScheduledProfileExecutionServic
}
}
// Aggiungi External ID Relationships (per Salesforce)
if (externalIdRelationships != null && externalIdRelationships.Any())
{
foreach (var relationship in externalIdRelationships)
{
if (!string.IsNullOrWhiteSpace(relationship.SourceField) &&
sourceRecord.ContainsKey(relationship.SourceField))
{
var sourceValue = sourceRecord[relationship.SourceField];
var transformedValue = TransformValueForRest(sourceValue);
if (transformedValue != null)
{
// Crea il dizionario annidato per l'External ID Relationship
// Formato: { "Account__r": { "Country__c": "US" } }
var externalIdObject = new Dictionary<string, object>
{
{ relationship.ExternalIdField, transformedValue }
};
restData[relationship.RelationshipName] = externalIdObject;
_logger.LogDebug("Aggiunta External ID Relationship: {RelationshipName} → {ExternalIdField} = {Value}",
relationship.RelationshipName, relationship.ExternalIdField, transformedValue);
}
}
}
}
return restData;
}