[Feature/Perf] Ottimizzazioni bulk pre-discovery, batch deletion sync e supporto OLE DB / Salesforce client_credentials
## Bulk Pre-Discovery e riduzione query SQLite/SOQL
### KeyAssociationService — FindAssociationsByKeyValuesBulkAsync (nuovo)
- Aggiunta query bulk 'WHERE KeyValue IN (...)' per recuperare N associazioni con 1 sola query SQLite
(chunking a 500 chiavi per rispettare il limite ~999 parametri di SQLite)
- Aggiunta interfaccia IKeyAssociationService e delegata in DataConnectionCredentialService / IDataConnectionCredentialService
### AssociationService — BatchFindOrCreateAssociationsAsync (nuovo)
- Nuovo metodo bulk che sostituisce i loop per-record durante l'analisi composite:
1) 1 query SQLite bulk per tutte le chiavi
2) Per le chiavi non trovate: SOQL 'IN (...)' su Salesforce in chunk da 200 via BatchExecuteQueriesAsync
(ceil(K/25) HTTP Composite call invece di K singole)
3) Salvataggio parallelo delle associazioni pre-discovery scoperte
- Fallback per-record automatico per client REST non Salesforce
- Aggiornata interfaccia IAssociationService con documentazione XML completa
### DataCoupler.razor.cs — STEP A/B nel flusso COMPOSITE
- Pre-Discovery spostata FUORI dal loop parallelo (STEP A, prima dell'analisi)
- associationsByKey pre-popolato con BatchFindOrCreateAssociationsAsync
- STEP B: il loop parallelo usa TryGetValue O(1) invece di query async per record
- Rimozione blocco ~40 righe di per-record lookup / fallback duplicati
## Salesforce Composite API — Batch Delete e Patch
### SalesforceServiceClient — metodi batch (nuovi)
- BatchDeleteEntitiesAsync: elimina N record con ceil(N/25) Composite call invece di N
- BatchPatchSingleFieldAsync: aggiorna un singolo campo su N record tramite BatchUpdateEntitiesAsync
### DeletionSyncService — refactoring batch
- ExecuteBatchedSalesforceDeletionsAsync: orchestrazione batch per Delete / Deactivate / Mark su Salesforce
- ExecuteSequentialDeletionsAsync: loop sequenziale esistente estratto in metodo riutilizzabile
- Dispatcher: Salesforce -> batch Composite, altri client REST -> sequenziale
## Supporto OLE DB (database)
### DatabaseSchemaProviderFactory
- Aggiunto case DatabaseType.OleDb -> new OleDbSchemaProvider() nel factory switch
### DatabaseMethod.cs
- Aggiunto metodo IsOleDbConnection() (parallelo a IsOdbcConnection())
- Query validation e manager temporaneo estesi a OLE DB oltre che ODBC
- GetLimitedQuery: aggiunto case OleDb -> 'SELECT TOP N FROM (subquery)'
## Salesforce OAuth2 — fix client_credentials
### CredentialService.cs
- Aggiunto 'GrantType' alla HashSet serviceSpecificKeys per preservarlo nella serializzazione AdditionalParameters
### DataConnectionCredentialService.cs
- Refactored BuildRestServiceOptions in helper statico riutilizzato da entrambi i metodi GetRestServiceOptions
- Mapping coerente ClientId/ClientSecret/GrantType per Salesforce (allineato a DataConnectionFactory)
- TestSalesforceOAuthLogin: branch esplicito per client_credentials (no username/password/token)
con validazione preventiva ClientId+ClientSecret obbligatori
- Log flow label (password|client_credentials) in tutti i messaggi di autenticazione
## VS Code tasks
### .vscode/tasks.json
- Rimosso task generico 'Publish Data_Coupler'
- Aggiunti due task separati: win-x64 e win-x86, entrambi SingleFile + Self-Contained + ReadyToRun
This commit is contained in:
@@ -806,11 +806,11 @@ public class CredentialService : ICredentialService
|
||||
}
|
||||
|
||||
// Copia tutti i parametri che non sono specifici del servizio
|
||||
var serviceSpecificKeys = new HashSet<string>
|
||||
{
|
||||
var serviceSpecificKeys = new HashSet<string>
|
||||
{
|
||||
"CompanyDatabase", "Language", "Version", "UseTrustedConnection",
|
||||
"SecurityToken", "ClientId", "ClientSecret", "ApiVersion",
|
||||
"IsSandbox", "UseSoapApi", "RefreshToken", "AccessToken", "TokenExpiry"
|
||||
"SecurityToken", "ClientId", "ClientSecret", "ApiVersion",
|
||||
"IsSandbox", "UseSoapApi", "GrantType", "RefreshToken", "AccessToken", "TokenExpiry"
|
||||
};
|
||||
|
||||
foreach (var param in additionalParams)
|
||||
|
||||
@@ -112,6 +112,16 @@ public interface IKeyAssociationService
|
||||
/// </summary>
|
||||
Task<KeyAssociation?> FindAssociationByKeyValueParallelAsync(string keyValue);
|
||||
|
||||
/// <summary>
|
||||
/// Versione bulk: ricerca in un colpo solo tutte le associazioni attive per la combinazione
|
||||
/// (KeyValue ∈ keyValues, DestinationEntity, RestCredentialName) usando una query SQL IN(...).
|
||||
/// Riduce drasticamente le query SQLite quando si processano molti record.
|
||||
/// </summary>
|
||||
Task<Dictionary<string, KeyAssociation>> FindAssociationsByKeyValuesBulkAsync(
|
||||
IEnumerable<string> keyValues,
|
||||
string destinationEntity,
|
||||
string restCredentialName);
|
||||
|
||||
/// <summary>
|
||||
/// Versione thread-safe per operazioni parallele - Elimina associazione
|
||||
/// </summary>
|
||||
|
||||
@@ -341,9 +341,9 @@ public class KeyAssociationService : IKeyAssociationService
|
||||
var options = new DbContextOptionsBuilder<CredentialDbContext>()
|
||||
.UseSqlite(_context.Database.GetConnectionString())
|
||||
.Options;
|
||||
|
||||
|
||||
using var parallelContext = new CredentialDbContext(options);
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
return await parallelContext.KeyAssociations
|
||||
@@ -358,6 +358,63 @@ public class KeyAssociationService : IKeyAssociationService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bulk lookup delle associazioni: una sola query con WHERE KeyValue IN (...).
|
||||
/// Per N chiavi sostituisce fino a 2N query SQLite del flusso per-record.
|
||||
/// </summary>
|
||||
public async Task<Dictionary<string, KeyAssociation>> FindAssociationsByKeyValuesBulkAsync(
|
||||
IEnumerable<string> keyValues,
|
||||
string destinationEntity,
|
||||
string restCredentialName)
|
||||
{
|
||||
var distinctKeys = keyValues
|
||||
.Where(k => !string.IsNullOrEmpty(k))
|
||||
.Distinct()
|
||||
.ToList();
|
||||
|
||||
if (distinctKeys.Count == 0)
|
||||
return new Dictionary<string, KeyAssociation>(StringComparer.Ordinal);
|
||||
|
||||
try
|
||||
{
|
||||
// SQLite ha un limite hardcoded di ~999 parametri per query: chunk per sicurezza.
|
||||
const int chunkSize = 500;
|
||||
var result = new Dictionary<string, KeyAssociation>(StringComparer.Ordinal);
|
||||
|
||||
for (int i = 0; i < distinctKeys.Count; i += chunkSize)
|
||||
{
|
||||
var chunk = distinctKeys.Skip(i).Take(chunkSize).ToList();
|
||||
|
||||
var associations = await _context.KeyAssociations
|
||||
.AsNoTracking()
|
||||
.Where(ka => ka.IsActive &&
|
||||
ka.DestinationEntity == destinationEntity &&
|
||||
ka.RestCredentialName == restCredentialName &&
|
||||
chunk.Contains(ka.KeyValue))
|
||||
.ToListAsync();
|
||||
|
||||
// Se ci sono duplicati (KeyValue ripetuto), tieni il più recente
|
||||
foreach (var assoc in associations
|
||||
.GroupBy(a => a.KeyValue)
|
||||
.Select(g => g.OrderByDescending(a => a.UpdatedAt ?? a.CreatedAt).First()))
|
||||
{
|
||||
result[assoc.KeyValue] = assoc;
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogDebug("BULK: Ricerca associazioni completata - {Found}/{Total} match per {Entity}/{Credential}",
|
||||
result.Count, distinctKeys.Count, destinationEntity, restCredentialName);
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "BULK: Errore nella ricerca bulk delle associazioni ({Count} chiavi, Entity={Entity})",
|
||||
distinctKeys.Count, destinationEntity);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Versione thread-safe per operazioni parallele - Delete association
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user