refactor: Centralizzata logica Pre-Discovery in servizio dedicato
Creato AssociationService per eliminare duplicazione codice e migliorare manutenibilità
Nuovo servizio:
- Data_Coupler/Services/AssociationService.cs (276 righe)
* Interfaccia IAssociationService con metodi pubblici
* PreDiscoveryRequest DTO per parametri configurabili
* FindOrCreateAssociationAsync(): ricerca locale + Pre-Discovery REST
* IsPreDiscoveryAssociation(): verifica marker associazioni Pre-Discovery
Refactoring DataCoupler.razor.cs:
- Injected IAssociationService nel componente
- StartDataTransferOriginal(): ridotto da 98 a 20 righe (-78)
- StartDataTransferWithComposite(): ridotto da 93 a 20 righe (-73)
- Verifica Pre-Discovery: ridotto da 20 a 2 righe (-18)
- Sostituito logica inline con chiamate al servizio centralizzato
Refactoring ScheduledProfileExecutionService.cs:
- Injected IAssociationService nel costruttore
- ExecuteDataTransferWithCompositeAsync(): ridotto da 99 a 20 righe (-79)
- Verifica Pre-Discovery: ridotto da 20 a 2 righe (-18)
- Parametro IsScheduledTransfer=true per tracciabilità
Dependency Injection:
- Registrato IAssociationService in Program.cs come Scoped
- Disponibile per dependency injection in tutti i componenti
Vantaggi:
- Eliminata duplicazione: 3 implementazioni → 1 servizio centralizzato
- Codice ridotto di 266 righe (330 → 64 nelle chiamate)
- Manutenibilità: modifiche future in un solo file
- Testabilità: interfaccia facilmente mockabile per unit test
- Riusabilità: servizio disponibile per futuri componenti
- Separazione responsabilità: logica associazioni isolata
Comportamento invariato:
- Nessuna modifica alla logica Pre-Discovery esistente
- Compatibilità completa con database e API
- Stessi marker e metadata nelle associazioni create
Docs: PRE_DISCOVERY_REFACTORING.md
Build: ✅ Successo (0 errori, 25 warning pre-esistenti)
This commit is contained in:
@@ -1,8 +1,354 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using CredentialManager.Models;
|
||||
using CredentialManager.Services;
|
||||
using DataConnection.CredentialManagement.Interfaces;
|
||||
using DataConnection.REST.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Data_Coupler.Services;
|
||||
|
||||
public class AssociationService
|
||||
/// <summary>
|
||||
/// Servizio per la gestione delle associazioni tra record sorgente e destinazione.
|
||||
/// Include logica di Pre-Discovery per trovare record esistenti prima di creare duplicati.
|
||||
/// </summary>
|
||||
public class AssociationService : IAssociationService
|
||||
{
|
||||
private readonly IDataConnectionCredentialService _credentialService;
|
||||
private readonly ILogger<AssociationService> _logger;
|
||||
|
||||
public AssociationService(
|
||||
IDataConnectionCredentialService credentialService,
|
||||
ILogger<AssociationService> logger)
|
||||
{
|
||||
_credentialService = credentialService ?? throw new ArgumentNullException(nameof(credentialService));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trova o crea un'associazione tramite Pre-Discovery.
|
||||
/// Se non esiste un'associazione locale, cerca nella destinazione REST.
|
||||
/// Se trova un record, crea l'associazione automaticamente.
|
||||
/// </summary>
|
||||
/// <param name="request">Parametri per la ricerca/creazione associazione</param>
|
||||
/// <returns>Associazione esistente o appena creata, null se non trovata</returns>
|
||||
public async Task<KeyAssociation?> FindOrCreateAssociationAsync(PreDiscoveryRequest request)
|
||||
{
|
||||
if (request == null)
|
||||
throw new ArgumentNullException(nameof(request));
|
||||
|
||||
if (string.IsNullOrEmpty(request.SourceKey))
|
||||
{
|
||||
_logger.LogWarning("FindOrCreateAssociationAsync: SourceKey vuoto, skip");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 1: Cerca associazione esistente
|
||||
var existingAssociation = await FindExistingAssociationAsync(request);
|
||||
if (existingAssociation != null)
|
||||
{
|
||||
_logger.LogDebug("Associazione esistente trovata: ID={AssociationId}, DestinationId={DestinationId}",
|
||||
existingAssociation.Id, existingAssociation.DestinationId);
|
||||
return existingAssociation;
|
||||
}
|
||||
|
||||
// Step 2: Pre-Discovery nella destinazione REST
|
||||
if (request.EnablePreDiscovery)
|
||||
{
|
||||
var discoveredAssociation = await PerformPreDiscoveryAsync(request);
|
||||
if (discoveredAssociation != null)
|
||||
{
|
||||
return discoveredAssociation;
|
||||
}
|
||||
}
|
||||
|
||||
// Non trovata né in locale né in destinazione
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifica se un'associazione è stata creata dal Pre-Discovery
|
||||
/// controllando il campo AdditionalInfo
|
||||
/// </summary>
|
||||
public bool IsPreDiscoveryAssociation(KeyAssociation association)
|
||||
{
|
||||
if (association == null || string.IsNullOrEmpty(association.AdditionalInfo))
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
var additionalInfo = JsonSerializer.Deserialize<Dictionary<string, object>>(association.AdditionalInfo);
|
||||
if (additionalInfo != null && additionalInfo.ContainsKey("CreatedBy"))
|
||||
{
|
||||
var createdBy = additionalInfo["CreatedBy"]?.ToString();
|
||||
return createdBy == "PreDiscovery";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Errore parsing AdditionalInfo per AssociationId={Id}", association.Id);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#region Private Methods
|
||||
|
||||
/// <summary>
|
||||
/// Cerca un'associazione esistente nel database locale
|
||||
/// </summary>
|
||||
private async Task<KeyAssociation?> FindExistingAssociationAsync(PreDiscoveryRequest request)
|
||||
{
|
||||
_logger.LogDebug("Cerco associazione per KeyValue='{KeyValue}', Entity='{Entity}', Credential='{Credential}'",
|
||||
request.SourceKey, request.DestinationEntity, request.CredentialName);
|
||||
|
||||
// Cerca con tutti i parametri
|
||||
var association = request.UseParallelMethod
|
||||
? await _credentialService.FindKeyAssociationByValueParallelAsync(
|
||||
request.SourceKey, request.DestinationEntity, request.CredentialName)
|
||||
: await _credentialService.FindKeyAssociationByValueAsync(
|
||||
request.SourceKey, request.DestinationEntity, request.CredentialName);
|
||||
|
||||
// FALLBACK: Se non trovata, cerca solo per KeyValue
|
||||
if (association == null)
|
||||
{
|
||||
_logger.LogDebug("Associazione non trovata con parametri specifici, provo solo con KeyValue");
|
||||
|
||||
association = request.UseParallelMethod
|
||||
? await _credentialService.FindKeyAssociationByValueParallelAsync(request.SourceKey)
|
||||
: await _credentialService.FindKeyAssociationByValueAsync(request.SourceKey);
|
||||
|
||||
if (association != null)
|
||||
{
|
||||
// Verifica compatibilità
|
||||
if (association.DestinationEntity != request.DestinationEntity ||
|
||||
association.RestCredentialName != request.CredentialName)
|
||||
{
|
||||
_logger.LogDebug("Associazione non compatibile: Entity={FoundEntity} vs {ExpectedEntity}, Credential={FoundCred} vs {ExpectedCred}",
|
||||
association.DestinationEntity, request.DestinationEntity,
|
||||
association.RestCredentialName, request.CredentialName);
|
||||
association = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return association;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Esegue il Pre-Discovery cercando nella destinazione REST
|
||||
/// </summary>
|
||||
private async Task<KeyAssociation?> PerformPreDiscoveryAsync(PreDiscoveryRequest request)
|
||||
{
|
||||
_logger.LogInformation("PRE-DISCOVERY: Nessuna associazione trovata per '{KeyValue}'. Cerco nella destinazione...",
|
||||
request.SourceKey);
|
||||
|
||||
// Verifica che il campo chiave sia mappato
|
||||
if (!request.FieldMappings.TryGetValue(request.SourceKeyField, out var mappedDestinationFieldName))
|
||||
{
|
||||
_logger.LogWarning("PRE-DISCOVERY: Campo chiave '{SourceKeyField}' non trovato nei mappings. Skip discovery.",
|
||||
request.SourceKeyField);
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Prepara i campi di ricerca
|
||||
var searchFields = new Dictionary<string, object>
|
||||
{
|
||||
{ mappedDestinationFieldName, request.SourceKey }
|
||||
};
|
||||
|
||||
_logger.LogInformation("PRE-DISCOVERY: Cerco in '{Entity}' dove {Field} = '{Value}'",
|
||||
request.DestinationEntity, mappedDestinationFieldName, request.SourceKey);
|
||||
|
||||
// Cerca nella destinazione REST
|
||||
var existingEntities = await request.RestClient.FindEntitiesByKeysAsync(
|
||||
request.DestinationEntity, searchFields);
|
||||
|
||||
if (existingEntities == null || existingEntities.Count == 0)
|
||||
{
|
||||
_logger.LogInformation("PRE-DISCOVERY: Nessun record esistente trovato per KeyValue: '{KeyValue}'",
|
||||
request.SourceKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Trovato! Prendi il primo risultato
|
||||
var foundEntity = existingEntities[0];
|
||||
|
||||
// Estrai l'ID del record trovato
|
||||
var destinationId = ExtractDestinationId(foundEntity);
|
||||
if (string.IsNullOrEmpty(destinationId))
|
||||
{
|
||||
_logger.LogWarning("PRE-DISCOVERY: Record trovato ma senza ID valido per KeyValue: '{KeyValue}'",
|
||||
request.SourceKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.LogInformation("PRE-DISCOVERY: ✅ Trovato record esistente! KeyValue: '{KeyValue}' -> DestinationId: '{DestinationId}'",
|
||||
request.SourceKey, destinationId);
|
||||
|
||||
// Crea l'associazione
|
||||
var newAssociation = CreatePreDiscoveryAssociation(
|
||||
request, mappedDestinationFieldName, destinationId);
|
||||
|
||||
// Salva l'associazione
|
||||
var associationId = request.UseParallelMethod
|
||||
? await _credentialService.SaveKeyAssociationParallelAsync(newAssociation)
|
||||
: await _credentialService.SaveKeyAssociationAsync(newAssociation);
|
||||
|
||||
_logger.LogInformation("PRE-DISCOVERY: Associazione creata con ID: {AssociationId}", associationId);
|
||||
|
||||
newAssociation.Id = associationId;
|
||||
return newAssociation;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "PRE-DISCOVERY: Errore durante la ricerca nella destinazione per KeyValue: '{KeyValue}'",
|
||||
request.SourceKey);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estrae l'ID di destinazione dal record trovato
|
||||
/// </summary>
|
||||
private string? ExtractDestinationId(Dictionary<string, object> entity)
|
||||
{
|
||||
// Prova "Id" (case-sensitive)
|
||||
if (entity.ContainsKey("Id"))
|
||||
return entity["Id"]?.ToString();
|
||||
|
||||
// Prova "id" (lowercase)
|
||||
if (entity.ContainsKey("id"))
|
||||
return entity["id"]?.ToString();
|
||||
|
||||
// Prova case-insensitive
|
||||
var idKey = entity.Keys.FirstOrDefault(k => k.Equals("Id", StringComparison.OrdinalIgnoreCase));
|
||||
return idKey != null ? entity[idKey]?.ToString() : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Crea una nuova associazione con marker Pre-Discovery
|
||||
/// </summary>
|
||||
private KeyAssociation CreatePreDiscoveryAssociation(
|
||||
PreDiscoveryRequest request,
|
||||
string mappedDestinationFieldName,
|
||||
string destinationId)
|
||||
{
|
||||
var additionalInfo = new Dictionary<string, object>
|
||||
{
|
||||
{ "CreatedBy", "PreDiscovery" },
|
||||
{ "DiscoveredAt", DateTime.UtcNow },
|
||||
{ "MappingCount", request.FieldMappings.Count }
|
||||
};
|
||||
|
||||
// Aggiungi info su scheduled transfer se specificato
|
||||
if (request.IsScheduledTransfer)
|
||||
{
|
||||
additionalInfo.Add("ScheduledTransfer", true);
|
||||
}
|
||||
|
||||
// Aggiungi source type se specificato
|
||||
if (!string.IsNullOrEmpty(request.SourceType))
|
||||
{
|
||||
additionalInfo.Add("SourceType", request.SourceType);
|
||||
}
|
||||
|
||||
return new KeyAssociation
|
||||
{
|
||||
KeyValue = request.SourceKey,
|
||||
SourceKeyField = request.SourceKeyField,
|
||||
DestinationKeyField = request.DestinationKeyField ?? "Id",
|
||||
MappedDestinationField = mappedDestinationFieldName,
|
||||
DestinationEntity = request.DestinationEntity,
|
||||
DestinationId = destinationId,
|
||||
RestCredentialName = request.CredentialName,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
LastVerifiedAt = DateTime.UtcNow,
|
||||
IsActive = true,
|
||||
Data_Hash = request.CurrentDataHash,
|
||||
AdditionalInfo = JsonSerializer.Serialize(additionalInfo)
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interfaccia per il servizio di gestione associazioni
|
||||
/// </summary>
|
||||
public interface IAssociationService
|
||||
{
|
||||
Task<KeyAssociation?> FindOrCreateAssociationAsync(PreDiscoveryRequest request);
|
||||
bool IsPreDiscoveryAssociation(KeyAssociation association);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parametri per la ricerca/creazione di associazioni con Pre-Discovery
|
||||
/// </summary>
|
||||
public class PreDiscoveryRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Valore della chiave sorgente (es: "C00001")
|
||||
/// </summary>
|
||||
public string SourceKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Nome del campo chiave nella sorgente (es: "CardCode")
|
||||
/// </summary>
|
||||
public string SourceKeyField { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Nome dell'entità destinazione (es: "Account")
|
||||
/// </summary>
|
||||
public string DestinationEntity { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Nome della credenziale REST (es: "Salesforce_Prod")
|
||||
/// </summary>
|
||||
public string CredentialName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Campo ID nella destinazione (default: "Id")
|
||||
/// </summary>
|
||||
public string? DestinationKeyField { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Mappings campo sorgente -> campo destinazione
|
||||
/// </summary>
|
||||
public Dictionary<string, string> FieldMappings { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Client REST per effettuare la ricerca nella destinazione
|
||||
/// </summary>
|
||||
public IRestServiceClient RestClient { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Hash dei dati correnti da salvare nell'associazione
|
||||
/// </summary>
|
||||
public string? CurrentDataHash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Se abilitare il Pre-Discovery (default: true)
|
||||
/// </summary>
|
||||
public bool EnablePreDiscovery { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Se usare i metodi paralleli thread-safe (default: false)
|
||||
/// </summary>
|
||||
public bool UseParallelMethod { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Se la richiesta proviene da un trasferimento schedulato
|
||||
/// </summary>
|
||||
public bool IsScheduledTransfer { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Tipo di sorgente (database, file, etc.)
|
||||
/// </summary>
|
||||
public string? SourceType { get; set; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user