75a9bbb0c8
- Rimosso limite TOP 1000 in EFCoreDatabaseManager.GetAllRecordsAsync - Eliminati controlli di sicurezza con limiti automatici in DataCoupler - Aggiornata documentazione per riflettere estrazione senza limiti - Supporto completo per dataset di grandi dimensioni - Mantenuto batching automatico Salesforce (25 record/batch) in parallelo Ora il sistema supporta l'estrazione completa di tabelle e query custom senza restrizioni artificiali, ideale per migrazioni e use cases enterprise.
1170 lines
53 KiB
C#
1170 lines
53 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;
|
|
}
|
|
} /// <summary>
|
|
/// Finds entities by their key fields in Salesforce.
|
|
/// </summary>
|
|
/// <param name="entityName">The name of the SObject to search (e.g., "Account", "Contact").</param>
|
|
/// <param name="keyFields">The key fields and their values to match.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>A list of matching entities or an empty list if none found.</returns>
|
|
public override async Task<List<Dictionary<string, object>>> FindEntitiesByKeysAsync(string entityName, Dictionary<string, object> keyFields, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
Console.WriteLine($"--- Starting Salesforce Entity Search: {entityName} ---");
|
|
Console.WriteLine($"Key Fields: {string.Join(", ", keyFields.Select(kvp => $"{kvp.Key}={kvp.Value}"))}");
|
|
|
|
if (!await EnsureAuthenticatedAsync(cancellationToken))
|
|
{
|
|
Console.WriteLine("Authentication failed for entity search");
|
|
return new List<Dictionary<string, object>>();
|
|
}
|
|
|
|
// Costruisci la query SOQL
|
|
var whereConditions = keyFields.Select(kvp =>
|
|
{
|
|
var value = kvp.Value?.ToString() ?? "";
|
|
// Se il valore è una stringa, aggiungi le virgolette
|
|
if (kvp.Value is string)
|
|
{
|
|
value = $"'{value.Replace("'", "\\'")}'"; // Escape delle virgolette
|
|
}
|
|
return $"{kvp.Key} = {value}";
|
|
});
|
|
|
|
var query = $"SELECT Id FROM {entityName} WHERE {string.Join(" AND ", whereConditions)}";
|
|
Console.WriteLine($"SOQL Query: {query}");
|
|
|
|
var encodedQuery = Uri.EscapeDataString(query);
|
|
var queryEndpoint = $"/services/data/v59.0/query/?q={encodedQuery}"; var response = await GetAsync<SalesforceQueryResponse>(queryEndpoint, cancellationToken);
|
|
|
|
if (response?.Records != null)
|
|
{
|
|
var results = response.Records.Select(record =>
|
|
record as Dictionary<string, object> ?? new Dictionary<string, object>()
|
|
).ToList();
|
|
|
|
Console.WriteLine($"Found {results.Count} entities matching the key fields");
|
|
return results;
|
|
}
|
|
|
|
Console.WriteLine("No entities found matching the key fields");
|
|
return new List<Dictionary<string, object>>();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error during Salesforce entity search: {ex.Message}");
|
|
return new List<Dictionary<string, object>>();
|
|
}
|
|
} /// <summary>
|
|
/// Deletes an entity in Salesforce by its ID.
|
|
/// </summary>
|
|
/// <param name="entityName">The name of the SObject (e.g., "Account", "Contact").</param>
|
|
/// <param name="entityId">The ID of the entity to delete.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>True if deletion was successful, false otherwise.</returns>
|
|
public override async Task<bool> DeleteEntityAsync(string entityName, string entityId, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
Console.WriteLine($"--- Starting Salesforce Entity Delete: {entityName}/{entityId} ---");
|
|
|
|
if (!await EnsureAuthenticatedAsync(cancellationToken))
|
|
{
|
|
Console.WriteLine("Authentication failed for entity deletion");
|
|
return false;
|
|
}
|
|
|
|
var deleteEndpoint = $"/services/data/v59.0/sobjects/{entityName}/{entityId}";
|
|
|
|
// Salesforce usa DELETE HTTP method per eliminare record
|
|
var request = new HttpRequestMessage(HttpMethod.Delete, $"{_instanceUrl}{deleteEndpoint}");
|
|
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken);
|
|
|
|
var response = await _httpClient.SendAsync(request, cancellationToken);
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
Console.WriteLine($"Entity {entityName}/{entityId} deleted successfully");
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
Console.WriteLine($"Failed to delete entity {entityName}/{entityId}. Status: {response.StatusCode}, Error: {errorContent}");
|
|
return false;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error during Salesforce entity deletion: {ex.Message}");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Updates an existing entity in Salesforce by its ID.
|
|
/// </summary>
|
|
/// <param name="entityName">The name of the SObject (e.g., "Account", "Contact").</param>
|
|
/// <param name="entityId">The ID of the entity to update.</param>
|
|
/// <param name="entityData">The data to update as key-value pairs.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>The updated entity data or null if update failed.</returns>
|
|
public override async Task<Dictionary<string, object>?> UpdateEntityAsync(string entityName, string entityId, Dictionary<string, object> entityData, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
Console.WriteLine($"--- Starting Salesforce Entity Update: {entityName}/{entityId} ---");
|
|
|
|
if (!await EnsureAuthenticatedAsync(cancellationToken))
|
|
{
|
|
Console.WriteLine("Authentication failed for entity update");
|
|
return null;
|
|
}
|
|
|
|
var updateEndpoint = $"/services/data/v59.0/sobjects/{entityName}/{entityId}";
|
|
|
|
// Salesforce usa PATCH HTTP method per aggiornare record
|
|
var request = new HttpRequestMessage(HttpMethod.Patch, $"{_instanceUrl}{updateEndpoint}");
|
|
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken);
|
|
request.Content = JsonContent.Create(entityData);
|
|
|
|
var response = await _httpClient.SendAsync(request, cancellationToken);
|
|
|
|
if (response.IsSuccessStatusCode)
|
|
{
|
|
Console.WriteLine($"Entity {entityName}/{entityId} updated successfully");
|
|
|
|
// Ritorna i dati aggiornati includendo l'ID
|
|
var updatedData = new Dictionary<string, object>(entityData)
|
|
{
|
|
["Id"] = entityId
|
|
};
|
|
return updatedData;
|
|
}
|
|
else
|
|
{
|
|
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
Console.WriteLine($"Failed to update entity {entityName}/{entityId}. Status: {response.StatusCode}, Error: {errorContent}");
|
|
return null;
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error during Salesforce entity update: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensures the client is authenticated, attempting to authenticate if not already authenticated.
|
|
/// </summary>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>True if authentication is successful, false otherwise.</returns>
|
|
private async Task<bool> EnsureAuthenticatedAsync(CancellationToken cancellationToken = default)
|
|
{
|
|
if (IsAuthenticated())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
Console.WriteLine("Client not authenticated, attempting to authenticate...");
|
|
return await AuthenticateAsync(cancellationToken);
|
|
}
|
|
|
|
// --- 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>(); } /// <summary>
|
|
/// Finds entities by required fields to detect duplicates.
|
|
/// </summary>
|
|
/// <param name="entityName">The name of the entity to search.</param>
|
|
/// <param name="requiredFields">The required fields and their values to search for.</param>
|
|
/// <param name="cancellationToken">Cancellation token.</param>
|
|
/// <returns>A list of matching entities.</returns>
|
|
public override async Task<List<Dictionary<string, object>>> FindEntitiesByRequiredFieldsAsync(string entityName, Dictionary<string, object> requiredFields, CancellationToken cancellationToken = default)
|
|
{
|
|
try
|
|
{
|
|
Console.WriteLine($"--- Searching for duplicates in {entityName} by required fields ---");
|
|
|
|
if (!await EnsureAuthenticatedAsync(cancellationToken))
|
|
{
|
|
Console.WriteLine("Authentication failed for required fields search");
|
|
return new List<Dictionary<string, object>>();
|
|
}
|
|
|
|
if (!requiredFields.Any())
|
|
{
|
|
Console.WriteLine("No required fields provided for duplicate search");
|
|
return new List<Dictionary<string, object>>();
|
|
} // Build WHERE clause with required fields
|
|
var whereConditions = new List<string>();
|
|
foreach (var field in requiredFields)
|
|
{
|
|
if (field.Value != null)
|
|
{
|
|
var value = field.Value.ToString();
|
|
// Escape single quotes in string values
|
|
if (field.Value is string stringValue)
|
|
{
|
|
value = stringValue.Replace("'", "\\'");
|
|
whereConditions.Add($"{field.Key} = '{value}'");
|
|
}
|
|
else
|
|
{
|
|
whereConditions.Add($"{field.Key} = {value}");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!whereConditions.Any())
|
|
{
|
|
Console.WriteLine("No valid field values provided for duplicate search");
|
|
return new List<Dictionary<string, object>>();
|
|
}
|
|
|
|
var whereClause = string.Join(" AND ", whereConditions);
|
|
var query = $"SELECT Id, {string.Join(", ", requiredFields.Keys)} FROM {entityName} WHERE {whereClause}";
|
|
|
|
Console.WriteLine($"Executing duplicate search query: {query}");
|
|
|
|
var queryEndpoint = $"/services/data/v59.0/query?q={Uri.EscapeDataString(query)}";
|
|
var response = await GetAsync<SalesforceQueryResponse>($"{_instanceUrl}{queryEndpoint}", cancellationToken);
|
|
|
|
if (response?.Records != null)
|
|
{
|
|
Console.WriteLine($"Found {response.Records.Count} potential duplicates for required fields: {string.Join(", ", requiredFields.Select(kv => $"{kv.Key}={kv.Value}"))}");
|
|
return response.Records;
|
|
}
|
|
|
|
Console.WriteLine("No duplicates found");
|
|
return new List<Dictionary<string, object>>();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error during required fields search: {ex.Message}");
|
|
return new List<Dictionary<string, object>>();
|
|
}
|
|
}
|
|
|
|
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; }
|
|
}
|
|
|
|
private class SalesforceQueryResponse
|
|
{
|
|
[JsonPropertyName("records")]
|
|
public List<Dictionary<string, object>> Records { get; set; } = new List<Dictionary<string, object>>();
|
|
}
|
|
|
|
private class SalesforceCompositeRequest
|
|
{
|
|
[JsonPropertyName("compositeRequest")]
|
|
public List<SalesforceCompositeSubRequest> CompositeRequest { get; set; } = new List<SalesforceCompositeSubRequest>();
|
|
}
|
|
|
|
private class SalesforceCompositeSubRequest
|
|
{
|
|
[JsonPropertyName("method")]
|
|
public string Method { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("url")]
|
|
public string Url { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("referenceId")]
|
|
public string ReferenceId { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("body")]
|
|
public object Body { get; set; } = new object();
|
|
}
|
|
|
|
private class SalesforceCompositeResponse
|
|
{
|
|
[JsonPropertyName("compositeResponse")]
|
|
public List<SalesforceCompositeSubResponse> CompositeResponse { get; set; } = new List<SalesforceCompositeSubResponse>();
|
|
}
|
|
|
|
private class SalesforceCompositeSubResponse
|
|
{
|
|
[JsonPropertyName("referenceId")]
|
|
public string ReferenceId { get; set; } = string.Empty;
|
|
|
|
[JsonPropertyName("httpStatusCode")]
|
|
public int HttpStatusCode { get; set; }
|
|
|
|
[JsonPropertyName("body")]
|
|
public object Body { get; set; } = new object();
|
|
}
|
|
|
|
public class CompositeOperationResult
|
|
{
|
|
public string ReferenceId { get; set; } = string.Empty;
|
|
public string? EntityId { get; set; }
|
|
public int HttpStatusCode { get; set; }
|
|
public bool Success { get; set; }
|
|
public string ErrorMessage { get; set; } = string.Empty;
|
|
public Dictionary<string, object>? CreatedData { get; set; }
|
|
public Dictionary<string, object>? UpdatedData { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes multiple create operations using Salesforce Composite API with automatic batching
|
|
/// </summary>
|
|
/// <param name="entityName">The name of the SObject to create</param>
|
|
/// <param name="entityDataList">List of entity data to create</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>List of results for each create operation</returns>
|
|
public async Task<List<CompositeOperationResult>> BatchCreateEntitiesAsync(string entityName, List<Dictionary<string, object>> entityDataList, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!IsAuthenticated())
|
|
{
|
|
Console.WriteLine("Error: Not authenticated to Salesforce. Cannot perform batch create.");
|
|
return new List<CompositeOperationResult>();
|
|
}
|
|
|
|
if (!entityDataList.Any())
|
|
{
|
|
return new List<CompositeOperationResult>();
|
|
}
|
|
|
|
// Salesforce limit: max 25 operations per composite request
|
|
const int maxBatchSize = 25;
|
|
|
|
// Split into batches of 25
|
|
var batches = new List<(List<Dictionary<string, object>> batch, int startIndex, int batchNumber)>();
|
|
for (int i = 0; i < entityDataList.Count; i += maxBatchSize)
|
|
{
|
|
var batch = entityDataList.Skip(i).Take(maxBatchSize).ToList();
|
|
var batchNumber = (i / maxBatchSize) + 1;
|
|
batches.Add((batch, i, batchNumber));
|
|
}
|
|
|
|
var totalBatches = batches.Count;
|
|
Console.WriteLine($"--- Starting parallel processing of {totalBatches} batch(es) with {entityDataList.Count} total records ---");
|
|
|
|
// Execute all batches in parallel
|
|
var batchTasks = batches.Select(async b =>
|
|
{
|
|
Console.WriteLine($"--- Processing Batch {b.batchNumber}/{totalBatches}: {b.batch.Count} records (parallel) ---");
|
|
return await ExecuteCreateBatchAsync(entityName, b.batch, b.startIndex, cancellationToken);
|
|
});
|
|
|
|
var batchResults = await Task.WhenAll(batchTasks);
|
|
|
|
// Aggregate all results
|
|
var allResults = new List<CompositeOperationResult>();
|
|
foreach (var result in batchResults)
|
|
{
|
|
allResults.AddRange(result);
|
|
}
|
|
|
|
Console.WriteLine($"All batches completed: {allResults.Count(r => r.Success)} success, {allResults.Count(r => !r.Success)} failed");
|
|
return allResults;
|
|
}
|
|
|
|
private async Task<List<CompositeOperationResult>> ExecuteCreateBatchAsync(string entityName, List<Dictionary<string, object>> batch, int startIndex, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
// Salesforce Composite API endpoint
|
|
var compositeUri = $"{_instanceUrl}/services/data/v60.0/composite/";
|
|
|
|
// Build composite request
|
|
var compositeRequest = new SalesforceCompositeRequest();
|
|
|
|
for (int i = 0; i < batch.Count; i++)
|
|
{
|
|
var subrequest = new SalesforceCompositeSubRequest
|
|
{
|
|
Method = "POST",
|
|
Url = $"/services/data/v60.0/sobjects/{entityName}/",
|
|
ReferenceId = $"create_{startIndex + i}",
|
|
Body = batch[i]
|
|
};
|
|
compositeRequest.CompositeRequest.Add(subrequest);
|
|
}
|
|
|
|
var response = await _httpClient.PostAsJsonAsync(compositeUri, compositeRequest, cancellationToken);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
Console.WriteLine($"Salesforce Batch Create failed: {response.StatusCode}");
|
|
Console.WriteLine($"Error details: {errorContent}");
|
|
|
|
// Return error results for all operations in this batch
|
|
return batch.Select((_, index) => new CompositeOperationResult
|
|
{
|
|
ReferenceId = $"create_{startIndex + index}",
|
|
Success = false,
|
|
ErrorMessage = $"Batch operation failed: {response.StatusCode} - {errorContent}"
|
|
}).ToList();
|
|
}
|
|
|
|
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
var compositeResponse = JsonSerializer.Deserialize<SalesforceCompositeResponse>(responseContent);
|
|
|
|
var results = new List<CompositeOperationResult>();
|
|
|
|
if (compositeResponse?.CompositeResponse != null)
|
|
{
|
|
foreach (var subResponse in compositeResponse.CompositeResponse)
|
|
{
|
|
var result = new CompositeOperationResult
|
|
{
|
|
ReferenceId = subResponse.ReferenceId,
|
|
HttpStatusCode = subResponse.HttpStatusCode,
|
|
Success = subResponse.HttpStatusCode >= 200 && subResponse.HttpStatusCode < 300
|
|
};
|
|
|
|
if (result.Success && subResponse.Body != null)
|
|
{
|
|
if (subResponse.Body is JsonElement bodyElement)
|
|
{
|
|
var bodyDict = JsonSerializer.Deserialize<Dictionary<string, object>>(bodyElement.GetRawText());
|
|
result.CreatedData = bodyDict;
|
|
|
|
// Extract the created ID
|
|
if (bodyDict?.ContainsKey("id") == true)
|
|
{
|
|
result.EntityId = bodyDict["id"]?.ToString();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result.ErrorMessage = subResponse.Body?.ToString() ?? "Unknown error";
|
|
}
|
|
|
|
results.Add(result);
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error during Salesforce batch create: {ex.Message}");
|
|
|
|
// Return error results for all operations in this batch
|
|
return batch.Select((_, index) => new CompositeOperationResult
|
|
{
|
|
ReferenceId = $"create_{startIndex + index}",
|
|
Success = false,
|
|
ErrorMessage = ex.Message
|
|
}).ToList();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes multiple update operations using Salesforce Composite API with automatic batching
|
|
/// </summary>
|
|
/// <param name="entityName">The name of the SObject to update</param>
|
|
/// <param name="updateData">Dictionary where key is entityId and value is the data to update</param>
|
|
/// <param name="cancellationToken">Cancellation token</param>
|
|
/// <returns>List of results for each update operation</returns>
|
|
public async Task<List<CompositeOperationResult>> BatchUpdateEntitiesAsync(string entityName, Dictionary<string, Dictionary<string, object>> updateData, CancellationToken cancellationToken = default)
|
|
{
|
|
if (!IsAuthenticated())
|
|
{
|
|
Console.WriteLine("Error: Not authenticated to Salesforce. Cannot perform batch update.");
|
|
return new List<CompositeOperationResult>();
|
|
}
|
|
|
|
if (!updateData.Any())
|
|
{
|
|
return new List<CompositeOperationResult>();
|
|
}
|
|
|
|
// Salesforce limit: max 25 operations per composite request
|
|
const int maxBatchSize = 25;
|
|
var updateList = updateData.ToList();
|
|
|
|
// Split into batches of 25
|
|
var batches = new List<(Dictionary<string, Dictionary<string, object>> batch, int startIndex, int batchNumber)>();
|
|
for (int i = 0; i < updateList.Count; i += maxBatchSize)
|
|
{
|
|
var batch = updateList.Skip(i).Take(maxBatchSize).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
|
var batchNumber = (i / maxBatchSize) + 1;
|
|
batches.Add((batch, i, batchNumber));
|
|
}
|
|
|
|
var totalBatches = batches.Count;
|
|
Console.WriteLine($"--- Starting parallel processing of {totalBatches} update batch(es) with {updateList.Count} total records ---");
|
|
|
|
// Execute all batches in parallel
|
|
var batchTasks = batches.Select(async b =>
|
|
{
|
|
Console.WriteLine($"--- Processing Update Batch {b.batchNumber}/{totalBatches}: {b.batch.Count} records (parallel) ---");
|
|
return await ExecuteUpdateBatchAsync(entityName, b.batch, b.startIndex, cancellationToken);
|
|
});
|
|
|
|
var batchResults = await Task.WhenAll(batchTasks);
|
|
|
|
// Aggregate all results
|
|
var allResults = new List<CompositeOperationResult>();
|
|
foreach (var result in batchResults)
|
|
{
|
|
allResults.AddRange(result);
|
|
}
|
|
|
|
Console.WriteLine($"All update batches completed: {allResults.Count(r => r.Success)} success, {allResults.Count(r => !r.Success)} failed");
|
|
return allResults;
|
|
}
|
|
|
|
private async Task<List<CompositeOperationResult>> ExecuteUpdateBatchAsync(string entityName, Dictionary<string, Dictionary<string, object>> batch, int startIndex, CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
// Salesforce Composite API endpoint
|
|
var compositeUri = $"{_instanceUrl}/services/data/v60.0/composite/";
|
|
|
|
// Build composite request
|
|
var compositeRequest = new SalesforceCompositeRequest();
|
|
|
|
int index = 0;
|
|
foreach (var kvp in batch)
|
|
{
|
|
var entityId = kvp.Key;
|
|
var entityData = kvp.Value;
|
|
|
|
var subrequest = new SalesforceCompositeSubRequest
|
|
{
|
|
Method = "PATCH",
|
|
Url = $"/services/data/v60.0/sobjects/{entityName}/{entityId}",
|
|
ReferenceId = $"update_{startIndex + index}",
|
|
Body = entityData
|
|
};
|
|
compositeRequest.CompositeRequest.Add(subrequest);
|
|
index++;
|
|
}
|
|
|
|
var response = await _httpClient.PostAsJsonAsync(compositeUri, compositeRequest, cancellationToken);
|
|
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
var errorContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
Console.WriteLine($"Salesforce Batch Update failed: {response.StatusCode}");
|
|
Console.WriteLine($"Error details: {errorContent}");
|
|
|
|
// Return error results for all operations in this batch
|
|
return batch.Select((kvp, idx) => new CompositeOperationResult
|
|
{
|
|
ReferenceId = $"update_{startIndex + idx}",
|
|
EntityId = kvp.Key,
|
|
Success = false,
|
|
ErrorMessage = $"Batch operation failed: {response.StatusCode} - {errorContent}"
|
|
}).ToList();
|
|
}
|
|
|
|
var responseContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
|
var compositeResponse = JsonSerializer.Deserialize<SalesforceCompositeResponse>(responseContent);
|
|
|
|
var results = new List<CompositeOperationResult>();
|
|
|
|
if (compositeResponse?.CompositeResponse != null)
|
|
{
|
|
int resultIndex = 0;
|
|
foreach (var subResponse in compositeResponse.CompositeResponse)
|
|
{
|
|
var originalEntityId = batch.ElementAt(resultIndex).Key;
|
|
|
|
var result = new CompositeOperationResult
|
|
{
|
|
ReferenceId = subResponse.ReferenceId,
|
|
EntityId = originalEntityId,
|
|
HttpStatusCode = subResponse.HttpStatusCode,
|
|
Success = subResponse.HttpStatusCode >= 200 && subResponse.HttpStatusCode < 300
|
|
};
|
|
|
|
if (result.Success)
|
|
{
|
|
// For successful updates, create updated data with the ID
|
|
var originalData = batch.ElementAt(resultIndex).Value;
|
|
result.UpdatedData = new Dictionary<string, object>(originalData)
|
|
{
|
|
["Id"] = originalEntityId
|
|
};
|
|
}
|
|
else
|
|
{
|
|
result.ErrorMessage = subResponse.Body?.ToString() ?? "Unknown error";
|
|
}
|
|
|
|
results.Add(result);
|
|
resultIndex++;
|
|
}
|
|
}
|
|
|
|
return results;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"Error during Salesforce batch update: {ex.Message}");
|
|
|
|
// Return error results for all operations in this batch
|
|
return batch.Select((kvp, idx) => new CompositeOperationResult
|
|
{
|
|
ReferenceId = $"update_{startIndex + idx}",
|
|
EntityId = kvp.Key,
|
|
Success = false,
|
|
ErrorMessage = ex.Message
|
|
}).ToList();
|
|
}
|
|
}
|
|
}
|
|
} |