Files
Data-Coupler/DataConnection/REST/Implementations/SalesforceServiceClient.cs
T
Alessio Dal Santo a873dce31b fix: Risolto errore "Invalid object name" nel trasferimento dati e pulizia codice
- Modificato GetAllRecordsAsync per utilizzare la stessa connection string del discovery schema
- Aggiunto metodo CreateConnection per creare connessioni DB appropriate per tipo
- Migliorata gestione nomi tabelle con schema (es. "dbo.OCRD")
- Rimossi metodi obsoleti di creazione entità (UpdateEntityData, CreateNewEntity)
- Eliminati riferimenti a variabili non dichiarate (newEntityData, isCreatingEntity)
- Aggiunto logging debug per connection string e query SQL
- Completata implementazione trasferimento dati da database a REST API

Il trasferimento dati ora utilizza la stessa connessione per discovery e estrazione,
risolvendo problemi di accesso alle tabelle durante l'operazione di upsert.
2025-06-17 16:35:51 +02:00

568 lines
25 KiB
C#

using DataConnection.REST.Configuration;
using DataConnection.REST.Interfaces;
using DataConnection.REST.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
namespace DataConnection.REST.Implementations
{
/// <summary>
/// Client specific for Salesforce REST API.
/// Handles OAuth2 authentication and metadata discovery.
/// </summary>
public class SalesforceServiceClient : BaseRestServiceClient, IRestMetadataDiscovery
{
private string? _accessToken;
private string? _instanceUrl;
private DateTime _tokenExpiry;
public SalesforceServiceClient(HttpClient httpClient, RestServiceOptions options)
: base(httpClient, options)
{
}
/// <summary>
/// Authenticates with Salesforce using Username/Password OAuth2 flow.
/// </summary>
/// <param name="clientId">Connected App Consumer Key</param>
/// <param name="clientSecret">Connected App Consumer Secret</param>
/// <param name="username">Salesforce username</param>
/// <param name="password">Salesforce password + security token</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if authentication is successful</returns>
public async Task<bool> AuthenticateAsync(string clientId, string clientSecret, string username, string password, CancellationToken cancellationToken = default)
{
try
{
var tokenEndpoint = "/services/oauth2/token";
var tokenRequest = new List<KeyValuePair<string, string>>
{
new("grant_type", "password"),
new("client_id", clientId),
new("client_secret", clientSecret),
new("username", username),
new("password", password)
};
var formContent = new FormUrlEncodedContent(tokenRequest);
Console.WriteLine($"--- Salesforce Authentication Attempt ---");
Console.WriteLine($"Target URL: {_httpClient.BaseAddress}{tokenEndpoint}");
Console.WriteLine($"Username: {username}");
Console.WriteLine($"--- End Salesforce Authentication Attempt ---");
var response = await _httpClient.PostAsync(tokenEndpoint, formContent, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
Console.WriteLine($"Salesforce Authentication failed: {response.StatusCode}");
Console.WriteLine($"Error details: {errorContent}");
return false;
}
var tokenResponse = await response.Content.ReadFromJsonAsync<SalesforceTokenResponse>(cancellationToken: cancellationToken);
if (tokenResponse != null && !string.IsNullOrEmpty(tokenResponse.AccessToken))
{
_accessToken = tokenResponse.AccessToken;
_instanceUrl = tokenResponse.InstanceUrl;
_tokenExpiry = DateTime.UtcNow.AddSeconds(3600); // Default 1 hour, Salesforce doesn't always return expires_in
// Don't change BaseAddress - we'll use absolute URLs for API calls
// Store the instance URL for building complete URLs later
// Add Authorization header for future requests
_httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken);
Console.WriteLine($"Salesforce Authentication successful. Token expires around: {_tokenExpiry.ToLocalTime()}");
Console.WriteLine($"Instance URL: {_instanceUrl}");
return true;
}
Console.WriteLine("Salesforce Authentication response could not be parsed.");
return false;
}
catch (HttpRequestException ex)
{
Console.WriteLine($"HTTP Request Error during Salesforce Authentication: {ex.Message}");
if (ex.InnerException != null)
{
Console.WriteLine($"Inner Exception: {ex.InnerException.Message}");
}
return false;
}
catch (JsonException ex)
{
Console.WriteLine($"JSON Parsing Error during Salesforce Authentication: {ex.Message}");
return false;
}
catch (Exception ex)
{
Console.WriteLine($"Error during Salesforce Authentication: {ex.Message}");
return false;
}
} /// <summary>
/// Authenticates with Salesforce using the credentials from options.
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>True if authentication is successful</returns>
public override async Task<bool> AuthenticateAsync(CancellationToken cancellationToken = default)
{
// For Salesforce, we need ClientId, ClientSecret, Username, and Password
// These should be provided in the options
if (string.IsNullOrEmpty(_options.Username) || string.IsNullOrEmpty(_options.Password))
{
Console.WriteLine("Salesforce authentication requires username and password in options");
return false;
}
if (string.IsNullOrEmpty(_options.ApiKey) || string.IsNullOrEmpty(_options.AuthToken))
{
Console.WriteLine("Salesforce authentication requires ApiKey (ClientId) and AuthToken (ClientSecret) in options");
return false;
}
// Use the actual credentials from options
var clientId = _options.ApiKey; // ClientId should be in ApiKey field
var clientSecret = _options.AuthToken; // ClientSecret should be in AuthToken field
Console.WriteLine($"Using Salesforce credentials - ClientId: {clientId}, Username: {_options.Username}");
return await AuthenticateAsync(clientId, clientSecret, _options.Username, _options.Password, cancellationToken);
}
/// <summary>
/// Checks if the current access token is active.
/// </summary>
public bool IsAuthenticated()
{
return !string.IsNullOrEmpty(_accessToken) && DateTime.UtcNow < _tokenExpiry;
}
/// <summary>
/// Discovers SObjects (entities) and their fields from Salesforce.
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>A list of discovered entity information</returns>
public async Task<List<RestEntityInfo>> DiscoverEntitiesAsync(CancellationToken cancellationToken = default)
{
if (!IsAuthenticated())
{
Console.WriteLine("Error: Not authenticated to Salesforce. Cannot discover metadata.");
return new List<RestEntityInfo>();
}
var entities = new List<RestEntityInfo>();
try
{
// First, get list of all SObjects
var sobjectsEndpoint = $"{_instanceUrl}/services/data/v60.0/sobjects/";
var response = await _httpClient.GetAsync(sobjectsEndpoint, cancellationToken);
response.EnsureSuccessStatusCode();
var sobjectsResponse = await response.Content.ReadFromJsonAsync<SalesforceSObjectsResponse>(cancellationToken: cancellationToken); if (sobjectsResponse?.SObjects != null)
{
// For demo purposes, limit to first 20 objects to avoid too many API calls
var limitedSObjects = sobjectsResponse.SObjects.ToList();
// Process SObjects in parallel for better performance
var semaphore = new SemaphoreSlim(20, 20); // Limit concurrent requests to 5
var tasks = limitedSObjects.Where(sobject => !string.IsNullOrEmpty(sobject.Name))
.Select(async sobject =>
{
await semaphore.WaitAsync(cancellationToken);
try
{
// Get detailed field information for each SObject
var describeEndpoint = $"{_instanceUrl}/services/data/v60.0/sobjects/{sobject.Name}/describe/";
var describeResponse = await _httpClient.GetAsync(describeEndpoint, cancellationToken);
if (describeResponse.IsSuccessStatusCode)
{
var describeResult = await describeResponse.Content.ReadFromJsonAsync<SalesforceDescribeResponse>(cancellationToken: cancellationToken);
if (describeResult?.Fields != null)
{
var entityInfo = new RestEntityInfo
{
Name = sobject.Name
};
foreach (var field in describeResult.Fields)
{
if (string.IsNullOrEmpty(field.Name)) continue;
var propInfo = new RestPropertyInfo
{
Name = field.Name,
Type = field.Type ?? "string",
IsKey = field.Name.Equals("Id", StringComparison.OrdinalIgnoreCase)
};
entityInfo.Properties.Add(propInfo);
}
return entityInfo;
}
}
return null;
}
catch (Exception ex)
{
Console.WriteLine($"Error describing SObject {sobject.Name}: {ex.Message}");
return null;
}
finally
{
semaphore.Release();
}
});
var results = await Task.WhenAll(tasks);
entities.AddRange(results.Where(result => result != null)!);
}
}
catch (HttpRequestException ex)
{
Console.WriteLine($"HTTP Request Error during Salesforce metadata discovery: {ex.Message}");
}
catch (JsonException ex)
{
Console.WriteLine($"JSON Parsing Error during Salesforce metadata discovery: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Error during Salesforce metadata discovery: {ex.Message}");
}
return entities;
}
/// <summary>
/// Discovers a list of available SObjects from Salesforce without detailed field information.
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>A list of discovered SObject summaries</returns>
public async Task<List<RestEntitySummary>> DiscoverEntitySummariesAsync(CancellationToken cancellationToken = default)
{
if (!IsAuthenticated())
{
Console.WriteLine("Error: Not authenticated to Salesforce. Cannot discover metadata.");
return new List<RestEntitySummary>();
}
var entities = new List<RestEntitySummary>();
try
{
// Get list of all SObjects
var sobjectsEndpoint = $"{_instanceUrl}/services/data/v60.0/sobjects/";
var response = await _httpClient.GetAsync(sobjectsEndpoint, cancellationToken);
response.EnsureSuccessStatusCode();
var sobjectsResponse = await response.Content.ReadFromJsonAsync<SalesforceSObjectsResponse>(cancellationToken: cancellationToken);
if (sobjectsResponse?.SObjects != null)
{
foreach (var sobject in sobjectsResponse.SObjects)
{
if (string.IsNullOrEmpty(sobject.Name)) continue;
var entitySummary = new RestEntitySummary
{
Name = sobject.Name,
Label = sobject.Label ?? sobject.Name,
IsCustom = sobject.Custom,
EntityType = "SObject"
};
entities.Add(entitySummary);
}
}
}
catch (HttpRequestException ex)
{
Console.WriteLine($"HTTP Request Error during Salesforce metadata discovery: {ex.Message}");
}
catch (JsonException ex)
{
Console.WriteLine($"JSON Parsing Error during Salesforce metadata discovery: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Error during Salesforce metadata discovery: {ex.Message}");
}
return entities.OrderBy(e => e.Name).ToList();
}
/// <summary>
/// Discovers detailed information for a specific SObject including all its fields.
/// </summary>
/// <param name="entityName">The name of the SObject to get details for</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Detailed SObject information or null if not found</returns>
public async Task<RestEntityInfo?> DiscoverEntityDetailsAsync(string entityName, CancellationToken cancellationToken = default)
{
if (!IsAuthenticated())
{
Console.WriteLine("Error: Not authenticated to Salesforce. Cannot discover entity details.");
return null;
}
try
{
// Get detailed field information for the specific SObject
var describeEndpoint = $"{_instanceUrl}/services/data/v60.0/sobjects/{entityName}/describe/";
var describeResponse = await _httpClient.GetAsync(describeEndpoint, cancellationToken);
if (!describeResponse.IsSuccessStatusCode)
{
Console.WriteLine($"Failed to get details for SObject {entityName}: {describeResponse.StatusCode}");
return null;
}
var describeResult = await describeResponse.Content.ReadFromJsonAsync<SalesforceDescribeResponse>(cancellationToken: cancellationToken);
if (describeResult?.Fields != null)
{
var entityInfo = new RestEntityInfo
{
Name = entityName
};
foreach (var field in describeResult.Fields)
{
if (string.IsNullOrEmpty(field.Name)) continue;
var propInfo = new RestPropertyInfo
{
Name = field.Name,
Type = field.Type ?? "string",
IsKey = field.Name.Equals("Id", StringComparison.OrdinalIgnoreCase),
IsRequired = !field.Nillable,
MaxLength = field.Length > 0 ? field.Length : null
};
entityInfo.Properties.Add(propInfo);
}
return entityInfo;
}
}
catch (HttpRequestException ex)
{
Console.WriteLine($"HTTP Request Error during Salesforce entity details discovery: {ex.Message}");
}
catch (JsonException ex)
{
Console.WriteLine($"JSON Parsing Error during Salesforce entity details discovery: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Error during Salesforce entity details discovery: {ex.Message}");
}
return null;
}
/// <summary>
/// Creates a new SObject in Salesforce.
/// </summary>
/// <param name="entityName">The name of the SObject to create (e.g., "Account", "Contact").</param>
/// <param name="entityData">The data for the new SObject as key-value pairs.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The created entity data or null if creation failed.</returns>
public override async Task<Dictionary<string, object>?> CreateEntityAsync(string entityName, Dictionary<string, object> entityData, CancellationToken cancellationToken = default)
{
if (!IsAuthenticated())
{
Console.WriteLine("Error: Not authenticated to Salesforce. Cannot create entity.");
return null;
} // Salesforce REST API endpoint for creating SObjects
var createUri = $"{_instanceUrl}/services/data/v60.0/sobjects/{entityName}/";
try
{
Console.WriteLine($"--- Salesforce Entity Creation Attempt ---");
Console.WriteLine($"SObject: {entityName}");
Console.WriteLine($"Target URL: {createUri}");
Console.WriteLine($"Data: {System.Text.Json.JsonSerializer.Serialize(entityData)}");
var response = await _httpClient.PostAsJsonAsync(createUri, entityData, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
Console.WriteLine($"Salesforce Entity Creation failed: {response.StatusCode}");
Console.WriteLine($"Error details: {errorContent}");
Console.WriteLine($"--- End Salesforce Entity Creation Attempt (Failed) ---");
return null;
}
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
Console.WriteLine($"Salesforce Entity Creation successful");
Console.WriteLine($"Response: {responseContent}");
Console.WriteLine($"--- End Salesforce Entity Creation Attempt (Success) ---");
if (string.IsNullOrEmpty(responseContent))
return entityData; // Return original data if no response content
// Salesforce returns creation result with Id and success status
var creationResult = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(responseContent);
// Merge the original data with the creation result (which includes the new Id)
if (creationResult != null)
{
var result = new Dictionary<string, object>(entityData);
foreach (var kvp in creationResult)
{
result[kvp.Key] = kvp.Value;
}
return result;
}
return creationResult;
}
catch (HttpRequestException ex)
{
Console.WriteLine($"HTTP Request Error during Salesforce entity creation: {ex.Message}");
if (ex.InnerException != null)
{
Console.WriteLine($"Inner Exception: {ex.InnerException.Message}");
}
Console.WriteLine($"--- End Salesforce Entity Creation Attempt (Exception) ---");
return null;
}
catch (JsonException ex)
{
Console.WriteLine($"JSON Parsing Error during Salesforce entity creation: {ex.Message}");
Console.WriteLine($"--- End Salesforce Entity Creation Attempt (JsonException) ---");
return null;
}
catch (Exception ex)
{
Console.WriteLine($"Error during Salesforce entity creation: {ex.Message}");
Console.WriteLine($"--- End Salesforce Entity Creation Attempt (Exception) ---");
return null;
} }
public override async Task<Dictionary<string, object>?> UpsertEntityAsync(string entityName, Dictionary<string, object> entityData, CancellationToken cancellationToken = default)
{
// Per Salesforce, implementiamo upsert provando prima la creazione
// Se fallisce con un errore di duplicato, potremmo implementare logic di aggiornamento
// Per ora, semplicemente tentiamo la creazione
try
{
Console.WriteLine($"--- Starting Salesforce Entity Upsert: {entityName} ---");
Console.WriteLine($"Entity Data: {string.Join(", ", entityData.Select(kvp => $"{kvp.Key}={kvp.Value}"))}");
// Prima tenta la creazione
var result = await CreateEntityAsync(entityName, entityData, cancellationToken);
if (result != null)
{
Console.WriteLine($"Upsert completed successfully via CREATE for {entityName}");
return result;
}
// Se la creazione fallisce, potresti implementare qui la logica di aggiornamento
// Per ora, restituiamo null
Console.WriteLine($"Upsert failed for {entityName}");
return null;
}
catch (Exception ex)
{
Console.WriteLine($"Error during Salesforce entity upsert: {ex.Message}");
return null;
}
}
// --- Nested classes for deserializing Salesforce responses ---
private class SalesforceTokenResponse
{
[JsonPropertyName("access_token")]
public string AccessToken { get; set; } = string.Empty;
[JsonPropertyName("instance_url")]
public string InstanceUrl { get; set; } = string.Empty;
[JsonPropertyName("id")]
public string Id { get; set; } = string.Empty;
[JsonPropertyName("token_type")]
public string TokenType { get; set; } = string.Empty;
[JsonPropertyName("issued_at")]
public string IssuedAt { get; set; } = string.Empty;
[JsonPropertyName("signature")]
public string Signature { get; set; } = string.Empty;
}
private class SalesforceSObjectsResponse
{
[JsonPropertyName("encoding")]
public string Encoding { get; set; } = string.Empty;
[JsonPropertyName("maxBatchSize")]
public int MaxBatchSize { get; set; }
[JsonPropertyName("sobjects")]
public List<SalesforceSObject> SObjects { get; set; } = new List<SalesforceSObject>();
}
private class SalesforceSObject
{
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("label")]
public string Label { get; set; } = string.Empty;
[JsonPropertyName("custom")]
public bool Custom { get; set; }
[JsonPropertyName("keyPrefix")]
public string KeyPrefix { get; set; } = string.Empty;
}
private class SalesforceDescribeResponse
{
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("label")]
public string Label { get; set; } = string.Empty;
[JsonPropertyName("fields")]
public List<SalesforceField> Fields { get; set; } = new List<SalesforceField>();
}
private class SalesforceField
{
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("type")]
public string Type { get; set; } = string.Empty;
[JsonPropertyName("label")]
public string Label { get; set; } = string.Empty;
[JsonPropertyName("length")]
public int Length { get; set; }
[JsonPropertyName("nillable")]
public bool Nillable { get; set; }
[JsonPropertyName("unique")]
public bool Unique { get; set; }
}
}
}