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,277 @@
|
||||
# Correzione Logica MappedDestinationField
|
||||
|
||||
## 📋 Problema Identificato
|
||||
|
||||
Il campo `MappedDestinationField` nella tabella `KeyAssociations` non veniva popolato correttamente perché la logica di ricerca era invertita.
|
||||
|
||||
### ❌ Logica Errata Precedente
|
||||
|
||||
Il codice cercava di trovare il campo **destinazione** mappato al campo chiave **sorgente**:
|
||||
|
||||
```csharp
|
||||
// SBAGLIATO: Cercava il valore nel dictionary usando sourceKeyField come chiave
|
||||
if (fieldMappings.ContainsKey(currentSourceKeyField))
|
||||
{
|
||||
mappedDestinationField = fieldMappings[currentSourceKeyField];
|
||||
}
|
||||
```
|
||||
|
||||
**Problema**: Questo non aveva senso perché:
|
||||
- Il campo chiave sorgente (`SourceKeyField`) è già memorizzato separatamente
|
||||
- Non serviva sapere a cosa era mappato il campo chiave sorgente
|
||||
- Il mapping cambia per ogni trasferimento
|
||||
|
||||
## ✅ Logica Corretta Implementata
|
||||
|
||||
### Obiettivo del Campo
|
||||
|
||||
`MappedDestinationField` deve memorizzare il **campo sorgente** che è mappato al campo fisso **"DestinationId"** nella destinazione REST.
|
||||
|
||||
### Struttura del Mapping
|
||||
|
||||
Nel dictionary `fieldMappings`:
|
||||
- **Chiave**: Nome del campo nella sorgente (database, CSV, Excel)
|
||||
- **Valore**: Nome del campo nella destinazione (entità REST API)
|
||||
|
||||
Esempio:
|
||||
```csharp
|
||||
fieldMappings = new Dictionary<string, object>
|
||||
{
|
||||
{ "CodiceFiscale", "DestinationId" }, // Campo da memorizzare
|
||||
{ "Nome", "FirstName" },
|
||||
{ "Cognome", "LastName" }
|
||||
}
|
||||
```
|
||||
|
||||
In questo caso, `MappedDestinationField` deve contenere **"CodiceFiscale"**.
|
||||
|
||||
### Nuova Implementazione
|
||||
|
||||
```csharp
|
||||
// CORRETTO: Cerca quale campo sorgente è mappato a "DestinationId"
|
||||
var mappingToDestinationId = fieldMappings.FirstOrDefault(m => m.Value == "DestinationId");
|
||||
if (!string.IsNullOrEmpty(mappingToDestinationId.Key))
|
||||
{
|
||||
mappedSourceField = mappingToDestinationId.Key;
|
||||
Logger.LogDebug("Campo sorgente '{SourceField}' è mappato a 'DestinationId'", mappedSourceField);
|
||||
}
|
||||
```
|
||||
|
||||
## 🔧 File Modificati
|
||||
|
||||
### 1. `DataCoupler.razor.cs`
|
||||
|
||||
#### Metodo `CreateAssociationAsync` (linea ~2890)
|
||||
|
||||
**Prima:**
|
||||
```csharp
|
||||
// Trova il campo di destinazione mappato alla chiave sorgente
|
||||
string? mappedDestinationField = null;
|
||||
if (fieldMappings.ContainsKey(currentSourceKeyField))
|
||||
{
|
||||
mappedDestinationField = fieldMappings[currentSourceKeyField];
|
||||
}
|
||||
```
|
||||
|
||||
**Dopo:**
|
||||
```csharp
|
||||
// Trova il campo sorgente che è mappato a "DestinationId"
|
||||
string? mappedSourceField = null;
|
||||
var mappingToDestinationId = fieldMappings.FirstOrDefault(m => m.Value == "DestinationId");
|
||||
if (!string.IsNullOrEmpty(mappingToDestinationId.Key))
|
||||
{
|
||||
mappedSourceField = mappingToDestinationId.Key;
|
||||
}
|
||||
```
|
||||
|
||||
#### Metodo `StartDataTransferOriginal` (linea ~1400)
|
||||
|
||||
Stessa correzione applicata anche al metodo di trasferimento originale (non composite).
|
||||
|
||||
### 2. `ScheduledProfileExecutionService.cs`
|
||||
|
||||
#### Metodo `CreateAssociationAsync` (linea ~975)
|
||||
|
||||
**Prima:**
|
||||
```csharp
|
||||
int mappingCount = 0;
|
||||
// ... calcolo mappingCount ...
|
||||
|
||||
var association = new KeyAssociation
|
||||
{
|
||||
// ... altri campi ...
|
||||
// MappedDestinationField non veniva popolato
|
||||
};
|
||||
```
|
||||
|
||||
**Dopo:**
|
||||
```csharp
|
||||
int mappingCount = 0;
|
||||
string? mappedSourceField = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(profile.FieldMappingJson))
|
||||
{
|
||||
var mappings = ParseFieldMappings(profile.FieldMappingJson);
|
||||
|
||||
// Cerca il campo sorgente mappato a "DestinationId"
|
||||
var mappingToDestinationId = mappings.FirstOrDefault(m => m.Value == "DestinationId");
|
||||
if (!string.IsNullOrEmpty(mappingToDestinationId.Key))
|
||||
{
|
||||
mappedSourceField = mappingToDestinationId.Key;
|
||||
}
|
||||
}
|
||||
|
||||
var association = new KeyAssociation
|
||||
{
|
||||
// ... altri campi ...
|
||||
MappedDestinationField = mappedSourceField, // Campo sorgente mappato a DestinationId
|
||||
};
|
||||
```
|
||||
|
||||
## 📊 Logging Diagnostico
|
||||
|
||||
### Log Implementati
|
||||
|
||||
```csharp
|
||||
// Traccia ricerca del campo
|
||||
Logger.LogDebug("MAPPING DEBUG: Cercando quale campo sorgente è mappato a 'DestinationId'");
|
||||
|
||||
// Mostra tutti i mapping disponibili
|
||||
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 a 'DestinationId'",
|
||||
mappedSourceField);
|
||||
|
||||
// Fallimento (warning)
|
||||
Logger.LogWarning("MAPPING DEBUG: Nessun campo sorgente mappato a 'DestinationId'! Il campo non verrà popolato.");
|
||||
|
||||
// Log creazione associazione
|
||||
Logger.LogDebug("COMPOSITE: Associazione creata con ID: {AssociationId} per record {RecordNumber} - Hash: {Hash}, MappedField: {MappedField}",
|
||||
associationId, recordNumber, finalDataHash, mappedSourceField ?? "N/A");
|
||||
```
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Pre-Requisiti per il Test
|
||||
|
||||
1. **Fermare l'applicazione** in esecuzione
|
||||
2. **Ricompilare** il progetto:
|
||||
```bash
|
||||
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** con campo mappato a `DestinationId`:
|
||||
- Esempio: `CodiceFiscale` (sorgente) → `DestinationId` (destinazione)
|
||||
|
||||
2. **Selezionare campo chiave** (può essere diverso dal campo mappato a DestinationId):
|
||||
- Esempio: `Email` come SourceKeyField
|
||||
|
||||
3. **Eseguire trasferimento dati**
|
||||
|
||||
4. **Verificare nei log**:
|
||||
```
|
||||
MAPPING DEBUG: Cercando quale campo sorgente è mappato a 'DestinationId'
|
||||
MAPPING DEBUG: Mappings disponibili: CodiceFiscale -> DestinationId, Nome -> FirstName, ...
|
||||
MAPPING DEBUG: Trovato mapping: campo sorgente 'CodiceFiscale' è mappato a 'DestinationId'
|
||||
COMPOSITE: Associazione creata con ID: 123 - MappedField: CodiceFiscale
|
||||
```
|
||||
|
||||
5. **Verificare nel database**:
|
||||
```sql
|
||||
SELECT
|
||||
SourceKeyField, -- Campo chiave sorgente (es. "Email")
|
||||
MappedDestinationField, -- Campo sorgente mappato a DestinationId (es. "CodiceFiscale")
|
||||
DestinationKeyField, -- Sempre "Id" o "DestinationId"
|
||||
KeyValue, -- Valore del campo chiave
|
||||
DestinationId -- ID generato dalla REST API
|
||||
FROM KeyAssociations
|
||||
ORDER BY CreatedAt DESC
|
||||
LIMIT 5;
|
||||
```
|
||||
|
||||
### Risultato Atteso
|
||||
|
||||
```
|
||||
SourceKeyField | MappedDestinationField | DestinationKeyField | KeyValue | DestinationId
|
||||
-----------------------|------------------------|---------------------|----------------------|---------------
|
||||
Email | CodiceFiscale | Id | user@example.com | ABC123XYZ
|
||||
Email | CodiceFiscale | Id | admin@example.com | DEF456UVW
|
||||
```
|
||||
|
||||
## 📝 Spiegazione Concettuale
|
||||
|
||||
### Differenza tra i Campi
|
||||
|
||||
| Campo | Descrizione | Esempio |
|
||||
|-------|-------------|---------|
|
||||
| `SourceKeyField` | Campo usato come chiave univoca nella sorgente | "Email" |
|
||||
| `KeyValue` | Valore specifico del campo chiave per questo record | "user@example.com" |
|
||||
| `MappedDestinationField` | Campo sorgente mappato a `DestinationId` nella REST API | "CodiceFiscale" |
|
||||
| `DestinationKeyField` | Campo chiave nella destinazione (sempre "Id" o "DestinationId") | "Id" |
|
||||
| `DestinationId` | ID univoco generato dalla REST API | "ABC123XYZ" |
|
||||
|
||||
### Perché è Importante
|
||||
|
||||
Durante il coupling, il sistema deve:
|
||||
|
||||
1. **Identificare record esistenti**: Usa `SourceKeyField` e `KeyValue`
|
||||
2. **Popolare DestinationId**: Usa il valore del campo sorgente specificato in `MappedDestinationField`
|
||||
3. **Evitare duplicati**: Verifica se esiste già un'associazione con lo stesso `KeyValue`
|
||||
|
||||
Esempio pratico:
|
||||
```
|
||||
Record CSV:
|
||||
- Email: "user@example.com" <- Usato come SourceKeyField per identificare il record
|
||||
- CodiceFiscale: "RSSMRA80A01H501U" <- Valore da mettere in DestinationId
|
||||
|
||||
Mapping:
|
||||
- CodiceFiscale → DestinationId <- MappedDestinationField = "CodiceFiscale"
|
||||
- Email → EmailAddress
|
||||
- Nome → FirstName
|
||||
|
||||
Associazione creata:
|
||||
- SourceKeyField: "Email"
|
||||
- KeyValue: "user@example.com"
|
||||
- MappedDestinationField: "CodiceFiscale"
|
||||
- DestinationId: "RSSMRA80A01H501U" <- Preso dal campo CodiceFiscale del record
|
||||
```
|
||||
|
||||
## ✅ 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] Aggiunta logging diagnostico in tutti i metodi
|
||||
- [x] Verifica assenza errori di compilazione
|
||||
- [ ] Test con trasferimento reale
|
||||
- [ ] Verifica popolamento campo nel database
|
||||
- [ ] Verifica log output corretto
|
||||
|
||||
## 🎯 Prossimi Passi
|
||||
|
||||
1. **Fermare applicazione in esecuzione**
|
||||
2. **Ricompilare progetto**
|
||||
3. **Riavviare applicazione**
|
||||
4. **Eseguire test trasferimento**
|
||||
5. **Verificare log output** (cercare "MAPPING DEBUG")
|
||||
6. **Query database** per confermare campo popolato
|
||||
|
||||
---
|
||||
|
||||
**Data Correzione**: 20 Ottobre 2025
|
||||
**Versione**: 1.0 - Correzione Logica MappedDestinationField
|
||||
Reference in New Issue
Block a user