fix: Correzione salvataggio campo MappedDestinationField in KeyAssociations
- Aggiunto campo MappedDestinationField al modello KeyAssociation per tracciare il campo destinazione mappato alla chiave sorgente
- Creata migration AddMappedDestinationFieldToKeyAssociation per aggiungere la colonna al database
- Implementata logica di popolamento in CreateAssociationAsync e StartDataTransferOriginal per salvare il campo destinazione mappato
- Aggiornato SaveAssociationParallelAsync per includere MappedDestinationField nelle query SQL UPDATE e INSERT
- Corretti indici parametri nella query UPDATE (da {7-9} a {8-10}) per includere il nuovo campo
- Aggiunta visualizzazione campo nell'interfaccia KeyAssociations (tabella, dettagli, export CSV)
- Implementato controllo validazione per impedire trasferimenti se il campo chiave non è mappato
- Aggiunto logging diagnostico dettagliato per debug del mapping dei campi
- Aggiornato ScheduledProfileExecutionService per popolare MappedDestinationField nelle esecuzioni schedulate
- Rimosso file BackgroundServices.cs obsoleto
- Documentazione completa creata (4 markdown files)
Fixes: Campo MappedDestinationField rimaneva NULL perché le query SQL raw non includevano il nuovo campo
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
# Correzione Finale MappedDestinationField
|
||||
|
||||
## 📋 Problema Risolto
|
||||
|
||||
Il campo `MappedDestinationField` nella tabella `KeyAssociations` deve memorizzare il **campo destinazione (REST API)** che è mappato al campo chiave sorgente.
|
||||
|
||||
## ✅ Logica Corretta Implementata
|
||||
|
||||
### Obiettivo del Campo
|
||||
|
||||
`MappedDestinationField` memorizza il **campo destinazione nella REST API** che corrisponde al campo chiave sorgente selezionato dall'utente.
|
||||
|
||||
### Esempio Pratico
|
||||
|
||||
**Scenario:**
|
||||
```
|
||||
Sorgente (CSV/Database):
|
||||
- Email: "user@example.com" <- Campo chiave selezionato (SourceKeyField)
|
||||
- Nome: "Mario"
|
||||
- Cognome: "Rossi"
|
||||
- CodiceFiscale: "RSSMRA80A01H501U"
|
||||
|
||||
Mappings configurati dall'utente:
|
||||
Email → EmailAddress <- MappedDestinationField deve essere "EmailAddress"
|
||||
Nome → FirstName
|
||||
Cognome → LastName
|
||||
CodiceFiscale → TaxCode
|
||||
|
||||
Campo chiave selezionato: Email
|
||||
```
|
||||
|
||||
**Risultato nel database:**
|
||||
```
|
||||
KeyAssociation:
|
||||
- SourceKeyField: "Email" <- Campo sorgente usato come chiave
|
||||
- KeyValue: "user@example.com" <- Valore del campo chiave
|
||||
- MappedDestinationField: "EmailAddress" <- Campo destinazione mappato
|
||||
- DestinationKeyField: "Id" <- Campo ID nella REST API
|
||||
- DestinationId: "ABC123" <- ID generato dalla REST API
|
||||
```
|
||||
|
||||
## 🔧 Implementazione Corretta
|
||||
|
||||
### Logica di Ricerca
|
||||
|
||||
```csharp
|
||||
// Trova il campo destinazione (REST API) mappato al campo chiave sorgente
|
||||
string? mappedDestinationField = null;
|
||||
|
||||
// Usa TryGetValue per cercare nel dictionary
|
||||
if (fieldMappings.TryGetValue(sourceKeyField, out var destinationFieldName))
|
||||
{
|
||||
// Trovato! destinationFieldName contiene il campo destinazione
|
||||
mappedDestinationField = destinationFieldName;
|
||||
Logger.LogDebug("Campo sorgente '{SourceField}' è mappato al campo destinazione '{DestField}'",
|
||||
sourceKeyField, mappedDestinationField);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Non trovato nei mappings
|
||||
Logger.LogWarning("Campo chiave sorgente '{SourceKeyField}' NON trovato nei mappings!",
|
||||
sourceKeyField);
|
||||
}
|
||||
```
|
||||
|
||||
### Dictionary Structure
|
||||
|
||||
```csharp
|
||||
// fieldMappings è strutturato come:
|
||||
Dictionary<string, string> fieldMappings = new()
|
||||
{
|
||||
// Chiave = Campo Sorgente → Valore = Campo Destinazione
|
||||
{ "Email", "EmailAddress" }, // sourceKeyField="Email" → MappedDestinationField="EmailAddress"
|
||||
{ "Nome", "FirstName" },
|
||||
{ "Cognome", "LastName" },
|
||||
{ "CodiceFiscale", "TaxCode" }
|
||||
};
|
||||
```
|
||||
|
||||
## 📝 File Modificati
|
||||
|
||||
### 1. `DataCoupler.razor.cs`
|
||||
|
||||
#### Metodo `CreateAssociationAsync` (linea ~2876)
|
||||
|
||||
```csharp
|
||||
private async Task CreateAssociationAsync(Dictionary<string, object> originalRecord, string entityId, int recordNumber, string? dataHash = null)
|
||||
{
|
||||
// ...
|
||||
var destinationKeyField = GetEntityIdField();
|
||||
|
||||
// Trova il campo destinazione (REST API) mappato al campo chiave sorgente
|
||||
string? mappedDestinationField = null;
|
||||
|
||||
Logger.LogDebug("MAPPING DEBUG: Cercando il campo destinazione mappato al campo chiave sorgente '{SourceKeyField}'", currentSourceKeyField);
|
||||
Logger.LogDebug("MAPPING DEBUG: Mappings disponibili: {Mappings}", string.Join(", ", fieldMappings.Select(m => $"{m.Key} -> {m.Value}")));
|
||||
|
||||
// Cerca nel dizionario il campo destinazione corrispondente al campo chiave sorgente
|
||||
if (fieldMappings.TryGetValue(currentSourceKeyField, out var destinationFieldName))
|
||||
{
|
||||
mappedDestinationField = destinationFieldName;
|
||||
Logger.LogDebug("MAPPING DEBUG: Trovato mapping: campo sorgente '{SourceField}' è mappato al campo destinazione '{DestField}'",
|
||||
currentSourceKeyField, mappedDestinationField);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("MAPPING DEBUG: Campo chiave sorgente '{SourceKeyField}' NON trovato nei mappings! Il campo MappedDestinationField non verrà popolato.",
|
||||
currentSourceKeyField);
|
||||
}
|
||||
|
||||
var association = new KeyAssociation
|
||||
{
|
||||
KeyValue = sourceKey,
|
||||
SourceKeyField = currentSourceKeyField,
|
||||
DestinationKeyField = destinationKeyField,
|
||||
MappedDestinationField = mappedDestinationField, // Campo destinazione mappato al campo chiave sorgente
|
||||
// ...
|
||||
};
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### Metodo `StartDataTransferOriginal` (linea ~1400)
|
||||
|
||||
Stessa logica applicata al metodo di trasferimento originale (non composite).
|
||||
|
||||
### 2. `ScheduledProfileExecutionService.cs`
|
||||
|
||||
#### Metodo `CreateAssociationAsync` (linea ~975)
|
||||
|
||||
```csharp
|
||||
// Calcola il MappingCount in modo sicuro e trova il campo destinazione mappato al campo chiave sorgente
|
||||
int mappingCount = 0;
|
||||
string? mappedDestinationField = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(profile.FieldMappingJson))
|
||||
{
|
||||
var mappings = ParseFieldMappings(profile.FieldMappingJson);
|
||||
mappingCount = mappings?.Count ?? 0;
|
||||
|
||||
// Cerca il campo destinazione mappato al campo chiave sorgente
|
||||
if (mappings != null && !string.IsNullOrEmpty(profile.SourceKeyField))
|
||||
{
|
||||
if (mappings.TryGetValue(profile.SourceKeyField, out var destinationFieldName))
|
||||
{
|
||||
mappedDestinationField = destinationFieldName;
|
||||
_logger.LogDebug("SCHEDULED MAPPING: Campo sorgente '{SourceField}' è mappato al campo destinazione '{DestField}'",
|
||||
profile.SourceKeyField, mappedDestinationField);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("SCHEDULED MAPPING: Campo chiave sorgente '{SourceKeyField}' NON trovato nei mappings del profilo {ProfileName}",
|
||||
profile.SourceKeyField, profile.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var association = new KeyAssociation
|
||||
{
|
||||
KeyValue = sourceKey,
|
||||
SourceKeyField = profile.SourceKeyField ?? "",
|
||||
DestinationKeyField = "Id",
|
||||
MappedDestinationField = mappedDestinationField, // Campo destinazione mappato al campo chiave sorgente
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
## 📊 Logging Diagnostico
|
||||
|
||||
### Log di Debug
|
||||
|
||||
```csharp
|
||||
// Inizio ricerca
|
||||
Logger.LogDebug("MAPPING DEBUG: Cercando il campo destinazione mappato al campo chiave sorgente '{SourceKeyField}'",
|
||||
sourceKeyField);
|
||||
|
||||
// Mostra tutti i mappings
|
||||
Logger.LogDebug("MAPPING DEBUG: Mappings disponibili: {Mappings}",
|
||||
string.Join(", ", fieldMappings.Select(m => $"{m.Key} -> {m.Value}")));
|
||||
|
||||
// Successo
|
||||
Logger.LogDebug("MAPPING DEBUG: Trovato mapping: campo sorgente '{SourceField}' è mappato al campo destinazione '{DestField}'",
|
||||
sourceKeyField, mappedDestinationField);
|
||||
|
||||
// Fallimento (warning)
|
||||
Logger.LogWarning("MAPPING DEBUG: Campo chiave sorgente '{SourceKeyField}' NON trovato nei mappings! Il campo MappedDestinationField non verrà popolato.",
|
||||
sourceKeyField);
|
||||
|
||||
// Creazione associazione
|
||||
Logger.LogDebug("COMPOSITE: Associazione creata con ID: {AssociationId} per record {RecordNumber} - Hash: {Hash}, MappedField: {MappedField}",
|
||||
associationId, recordNumber, finalDataHash, mappedDestinationField ?? "N/A");
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Pre-Requisiti
|
||||
|
||||
1. **Fermare l'applicazione** in esecuzione (attualmente blocca i file DLL)
|
||||
2. **Ricompilare** il progetto:
|
||||
```powershell
|
||||
dotnet build Data_Coupler.sln
|
||||
```
|
||||
3. **Configurare logging Debug** in `appsettings.Development.json`:
|
||||
```json
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Data_Coupler.Pages.DataCoupler": "Debug",
|
||||
"Data_Coupler.Services.ScheduledProfileExecutionService": "Debug"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Scenario di Test
|
||||
|
||||
1. **Configurare mapping**:
|
||||
- Sorgente: CSV con colonne `Email`, `Nome`, `Cognome`
|
||||
- Destinazione: Salesforce Contact con campi `EmailAddress`, `FirstName`, `LastName`
|
||||
- Mapping:
|
||||
- Email → EmailAddress
|
||||
- Nome → FirstName
|
||||
- Cognome → LastName
|
||||
|
||||
2. **Selezionare campo chiave**: `Email`
|
||||
|
||||
3. **Eseguire trasferimento dati**
|
||||
|
||||
4. **Verificare nei log**:
|
||||
```
|
||||
MAPPING DEBUG: Cercando il campo destinazione mappato al campo chiave sorgente 'Email'
|
||||
MAPPING DEBUG: Mappings disponibili: Email -> EmailAddress, Nome -> FirstName, Cognome -> LastName
|
||||
MAPPING DEBUG: Trovato mapping: campo sorgente 'Email' è mappato al campo destinazione 'EmailAddress'
|
||||
COMPOSITE: Associazione creata con ID: 123 - MappedField: EmailAddress
|
||||
```
|
||||
|
||||
5. **Verificare nel database**:
|
||||
```sql
|
||||
SELECT
|
||||
Id,
|
||||
SourceKeyField, -- 'Email'
|
||||
KeyValue, -- 'user@example.com'
|
||||
MappedDestinationField, -- 'EmailAddress' ← DEVE ESSERE POPOLATO!
|
||||
DestinationKeyField, -- 'Id'
|
||||
DestinationId, -- 'ABC123XYZ'
|
||||
DestinationEntity, -- 'Contact'
|
||||
RestCredentialName -- 'Salesforce_Prod'
|
||||
FROM KeyAssociations
|
||||
ORDER BY CreatedAt DESC
|
||||
LIMIT 5;
|
||||
```
|
||||
|
||||
### Risultato Atteso
|
||||
|
||||
```
|
||||
Id | SourceKeyField | KeyValue | MappedDestinationField | DestinationKeyField | DestinationId
|
||||
----|----------------|-------------------|------------------------|---------------------|---------------
|
||||
1 | Email | user@example.com | EmailAddress | Id | ABC123XYZ
|
||||
2 | Email | admin@example.com | EmailAddress | Id | DEF456UVW
|
||||
```
|
||||
|
||||
## 📐 Schema dei Campi
|
||||
|
||||
| Campo | Tipo | Descrizione | Esempio |
|
||||
|-------|------|-------------|---------|
|
||||
| `SourceKeyField` | Campo sorgente | Campo usato come chiave univoca nella sorgente | "Email" |
|
||||
| `KeyValue` | Valore | Valore specifico del campo chiave per questo record | "user@example.com" |
|
||||
| `MappedDestinationField` | Campo destinazione | **Campo REST API** mappato al campo chiave sorgente | "EmailAddress" |
|
||||
| `DestinationKeyField` | Campo destinazione | Campo ID nella destinazione REST API (sempre "Id") | "Id" |
|
||||
| `DestinationId` | ID generato | ID univoco generato dalla REST API dopo creazione | "ABC123XYZ" |
|
||||
|
||||
## 🔍 Perché è Importante
|
||||
|
||||
Il campo `MappedDestinationField` serve per:
|
||||
|
||||
1. **Tracciabilità**: Sapere quale campo REST API corrisponde alla chiave sorgente
|
||||
2. **Debugging**: Verificare il mapping applicato durante il trasferimento
|
||||
3. **Audit**: Documentare la configurazione utilizzata per ogni associazione
|
||||
4. **Ricostruzione**: Poter ricreare il mapping originale se necessario
|
||||
|
||||
### Caso d'Uso Reale
|
||||
|
||||
**Scenario**: Un utente vuole sapere quale campo Salesforce è stato usato per l'email quando ha fatto il coupling.
|
||||
|
||||
**Query:**
|
||||
```sql
|
||||
SELECT
|
||||
SourceKeyField, -- 'Email'
|
||||
MappedDestinationField -- 'EmailAddress'
|
||||
FROM KeyAssociations
|
||||
WHERE DestinationEntity = 'Contact'
|
||||
AND RestCredentialName = 'Salesforce_Prod'
|
||||
LIMIT 1;
|
||||
```
|
||||
|
||||
**Risposta**: "Il campo sorgente `Email` è stato mappato al campo Salesforce `EmailAddress`"
|
||||
|
||||
## ✅ Checklist Verifica
|
||||
|
||||
- [x] Correzione logica in `DataCoupler.razor.cs::CreateAssociationAsync`
|
||||
- [x] Correzione logica in `DataCoupler.razor.cs::StartDataTransferOriginal`
|
||||
- [x] Correzione logica in `ScheduledProfileExecutionService.cs::CreateAssociationAsync`
|
||||
- [x] Uso di `TryGetValue` per ricerca sicura nel dictionary
|
||||
- [x] Logging diagnostico completo
|
||||
- [x] Verifica assenza errori di compilazione
|
||||
- [ ] **Fermare applicazione in esecuzione**
|
||||
- [ ] **Ricompilare progetto**
|
||||
- [ ] **Riavviare applicazione**
|
||||
- [ ] **Test con trasferimento reale**
|
||||
- [ ] **Verificare log output** (cercare "MAPPING DEBUG")
|
||||
- [ ] **Query database** per confermare campo popolato
|
||||
|
||||
## 🎯 Prossimi Passi IMMEDIATI
|
||||
|
||||
1. ⛔ **FERMARE l'applicazione** in esecuzione (il processo blocca le DLL)
|
||||
2. 🔨 **Ricompilare**: `dotnet build Data_Coupler.sln`
|
||||
3. ▶️ **Riavviare** l'applicazione
|
||||
4. 🧪 **Eseguire test** di trasferimento con campo chiave mappato
|
||||
5. 📋 **Verificare log** per messaggio "Trovato mapping"
|
||||
6. 🔍 **Query database** per verificare `MappedDestinationField` popolato
|
||||
|
||||
---
|
||||
|
||||
**Data Correzione**: 20 Ottobre 2025
|
||||
**Versione**: 2.0 - Correzione Finale MappedDestinationField
|
||||
**Status**: ✅ Implementazione completa, pronto per test
|
||||
Reference in New Issue
Block a user