[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:
@@ -85,6 +85,14 @@ public interface IDataConnectionCredentialService
|
||||
Task<KeyAssociation?> FindKeyAssociationByValueParallelAsync(string keyValue);
|
||||
Task<bool> DeleteKeyAssociationParallelAsync(int id);
|
||||
|
||||
/// <summary>
|
||||
/// Bulk lookup associazioni - una sola query SQLite per N chiavi.
|
||||
/// </summary>
|
||||
Task<Dictionary<string, KeyAssociation>> FindKeyAssociationsByValuesBulkAsync(
|
||||
IEnumerable<string> keyValues,
|
||||
string destinationEntity,
|
||||
string restCredentialName);
|
||||
|
||||
// Deletion synchronization operations
|
||||
Task<int> MarkDeletedAssociationsAsync(List<string> sourceKeyValues, string destinationEntity, string restCredentialName);
|
||||
Task<List<KeyAssociation>> GetPendingDeletionsAsync(string destinationEntity, string restCredentialName);
|
||||
|
||||
@@ -168,16 +168,7 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
|
||||
if (credential == null)
|
||||
throw new InvalidOperationException($"REST API credential '{credentialName}' not found");
|
||||
|
||||
var options = new DataConnection.REST.Configuration.RestServiceOptions
|
||||
{
|
||||
BaseUrl = credential.BaseUrl,
|
||||
ApiKey = credential.ApiKey,
|
||||
Username = credential.Username,
|
||||
Password = credential.Password,
|
||||
AuthToken = credential.AuthToken,
|
||||
TimeoutSeconds = credential.TimeoutSeconds,
|
||||
IgnoreSslErrors = credential.IgnoreSslErrors
|
||||
};
|
||||
var options = BuildRestServiceOptions(credential);
|
||||
|
||||
_logger.LogDebug("Created RestServiceOptions for credential: {Name} ({BaseUrl})",
|
||||
credentialName, credential.BaseUrl);
|
||||
@@ -191,19 +182,42 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
|
||||
if (credential == null)
|
||||
throw new InvalidOperationException($"REST API credential with ID '{credentialId}' not found");
|
||||
|
||||
var options = BuildRestServiceOptions(credential);
|
||||
|
||||
_logger.LogDebug("Created RestServiceOptions for credential ID: {Id} ({BaseUrl})",
|
||||
credentialId, credential.BaseUrl);
|
||||
return options;
|
||||
}
|
||||
|
||||
private static DataConnection.REST.Configuration.RestServiceOptions BuildRestServiceOptions(RestApiCredential credential)
|
||||
{
|
||||
var options = new DataConnection.REST.Configuration.RestServiceOptions
|
||||
{
|
||||
BaseUrl = credential.BaseUrl,
|
||||
ApiKey = credential.ApiKey,
|
||||
Username = credential.Username,
|
||||
Password = credential.Password,
|
||||
AuthToken = credential.AuthToken,
|
||||
TimeoutSeconds = credential.TimeoutSeconds,
|
||||
IgnoreSslErrors = credential.IgnoreSslErrors
|
||||
};
|
||||
|
||||
_logger.LogDebug("Created RestServiceOptions for credential ID: {Id} ({BaseUrl})",
|
||||
credentialId, credential.BaseUrl);
|
||||
// Mapping coerente con DataConnectionFactory.CreateRestServiceClientAsync
|
||||
switch (credential.ServiceType)
|
||||
{
|
||||
case RestServiceType.Salesforce:
|
||||
options.ApiKey = credential.ClientId;
|
||||
options.AuthToken = credential.ClientSecret;
|
||||
options.SalesforceGrantType = credential.GrantType;
|
||||
break;
|
||||
case RestServiceType.SapB1ServiceLayer:
|
||||
options.ApiKey = credential.CompanyDatabase;
|
||||
options.AuthToken = credential.AuthToken;
|
||||
break;
|
||||
default:
|
||||
options.ApiKey = credential.ApiKey;
|
||||
options.AuthToken = credential.AuthToken;
|
||||
break;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
@@ -550,8 +564,8 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Testing Salesforce authentication for {Name} ({BaseUrl})",
|
||||
credential.Name, credential.BaseUrl);
|
||||
_logger.LogInformation("Testing Salesforce authentication for {Name} ({BaseUrl}, GrantType={GrantType})",
|
||||
credential.Name, credential.BaseUrl, credential.GrantType);
|
||||
|
||||
_logger.LogDebug("Salesforce credential details: Username={Username}, HasPassword={HasPassword}, HasSecurityToken={HasSecurityToken}, HasClientId={HasClientId}, HasClientSecret={HasClientSecret}",
|
||||
credential.Username, !string.IsNullOrEmpty(credential.Password), !string.IsNullOrEmpty(credential.SecurityToken),
|
||||
@@ -560,49 +574,69 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
|
||||
using var httpClient = new HttpClient();
|
||||
httpClient.Timeout = TimeSpan.FromSeconds(credential.TimeoutSeconds);
|
||||
|
||||
// Test di autenticazione OAuth2
|
||||
var tokenUrl = credential.BaseUrl.TrimEnd('/') + "/services/oauth2/token";
|
||||
var tokenData = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new("grant_type", "password"),
|
||||
new("username", credential.Username ?? "")
|
||||
};
|
||||
List<KeyValuePair<string, string>> tokenData;
|
||||
string flowLabel;
|
||||
|
||||
// Aggiungiamo password + security token se disponibile
|
||||
var password = credential.Password ?? "";
|
||||
if (!string.IsNullOrEmpty(credential.SecurityToken))
|
||||
if (credential.GrantType == CredentialManager.Models.SalesforceGrantType.ClientCredentials)
|
||||
{
|
||||
password += credential.SecurityToken;
|
||||
}
|
||||
tokenData.Add(new("password", password));
|
||||
// Client Credentials flow — server-to-server, no user
|
||||
if (string.IsNullOrEmpty(credential.ClientId) || string.IsNullOrEmpty(credential.ClientSecret))
|
||||
{
|
||||
return (false, "Flusso client_credentials richiede ClientId e ClientSecret configurati.");
|
||||
}
|
||||
|
||||
// Aggiungiamo client credentials se disponibili
|
||||
if (!string.IsNullOrEmpty(credential.ClientId))
|
||||
{
|
||||
tokenData.Add(new("client_id", credential.ClientId));
|
||||
tokenData = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new("grant_type", "client_credentials"),
|
||||
new("client_id", credential.ClientId),
|
||||
new("client_secret", credential.ClientSecret)
|
||||
};
|
||||
flowLabel = "client_credentials";
|
||||
}
|
||||
if (!string.IsNullOrEmpty(credential.ClientSecret))
|
||||
else
|
||||
{
|
||||
tokenData.Add(new("client_secret", credential.ClientSecret));
|
||||
// Password flow (default)
|
||||
var password = credential.Password ?? "";
|
||||
if (!string.IsNullOrEmpty(credential.SecurityToken))
|
||||
{
|
||||
password += credential.SecurityToken;
|
||||
}
|
||||
|
||||
tokenData = new List<KeyValuePair<string, string>>
|
||||
{
|
||||
new("grant_type", "password"),
|
||||
new("username", credential.Username ?? ""),
|
||||
new("password", password)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(credential.ClientId))
|
||||
{
|
||||
tokenData.Add(new("client_id", credential.ClientId));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(credential.ClientSecret))
|
||||
{
|
||||
tokenData.Add(new("client_secret", credential.ClientSecret));
|
||||
}
|
||||
flowLabel = "password";
|
||||
}
|
||||
|
||||
_logger.LogDebug("Posting to Salesforce token URL: {TokenUrl}", tokenUrl);
|
||||
_logger.LogDebug("Posting to Salesforce token URL: {TokenUrl} (flow={Flow})", tokenUrl, flowLabel);
|
||||
|
||||
var tokenContent = new FormUrlEncodedContent(tokenData);
|
||||
var response = await httpClient.PostAsync(tokenUrl, tokenContent);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var responseContent = await response.Content.ReadAsStringAsync();
|
||||
_logger.LogInformation("Salesforce authentication successful for {Name}", credential.Name);
|
||||
return (true, $"Autenticazione Salesforce riuscita!\n\nDettagli:\n- Login URL: {credential.BaseUrl}\n- API Version: {credential.ApiVersion}\n- Sandbox: {credential.IsSandbox}\n- Tipo Auth: OAuth2\n- Timeout: {credential.TimeoutSeconds}s");
|
||||
_logger.LogInformation("Salesforce authentication ({Flow}) successful for {Name}", flowLabel, credential.Name);
|
||||
return (true, $"Autenticazione Salesforce riuscita!\n\nDettagli:\n- Login URL: {credential.BaseUrl}\n- API Version: {credential.ApiVersion}\n- Sandbox: {credential.IsSandbox}\n- Tipo Auth: OAuth2 ({flowLabel})\n- Timeout: {credential.TimeoutSeconds}s");
|
||||
}
|
||||
else
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
_logger.LogWarning("Salesforce authentication failed for {Name}. Status: {StatusCode}, Response: {Response}",
|
||||
credential.Name, response.StatusCode, errorContent);
|
||||
return (false, $"Autenticazione Salesforce fallita. Status: {response.StatusCode}\nDettagli: {errorContent}");
|
||||
_logger.LogWarning("Salesforce authentication ({Flow}) failed for {Name}. Status: {StatusCode}, Response: {Response}",
|
||||
flowLabel, credential.Name, response.StatusCode, errorContent);
|
||||
return (false, $"Autenticazione Salesforce ({flowLabel}) fallita. Status: {response.StatusCode}\nDettagli: {errorContent}");
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
@@ -1074,6 +1108,14 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
|
||||
return await _keyAssociationService.FindAssociationByKeyValueParallelAsync(keyValue);
|
||||
}
|
||||
|
||||
public async Task<Dictionary<string, KeyAssociation>> FindKeyAssociationsByValuesBulkAsync(
|
||||
IEnumerable<string> keyValues,
|
||||
string destinationEntity,
|
||||
string restCredentialName)
|
||||
{
|
||||
return await _keyAssociationService.FindAssociationsByKeyValuesBulkAsync(keyValues, destinationEntity, restCredentialName);
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteKeyAssociationParallelAsync(int id)
|
||||
{
|
||||
return await _keyAssociationService.DeleteAssociationParallelAsync(id);
|
||||
|
||||
Reference in New Issue
Block a user