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 { /// /// Client specific for Salesforce REST API. /// Handles OAuth2 authentication and metadata discovery. /// public class SalesforceServiceClient : BaseRestServiceClient, IRestMetadataDiscovery { private string? _accessToken; private string? _instanceUrl; private DateTime _tokenExpiry; public SalesforceServiceClient(HttpClient httpClient, RestServiceOptions options) : base(httpClient, options) { } /// /// Authenticates with Salesforce using Username/Password OAuth2 flow. /// /// Connected App Consumer Key /// Connected App Consumer Secret /// Salesforce username /// Salesforce password + security token /// Cancellation token /// True if authentication is successful public async Task AuthenticateAsync(string clientId, string clientSecret, string username, string password, CancellationToken cancellationToken = default) { try { var tokenEndpoint = "/services/oauth2/token"; var tokenRequest = new List> { 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(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; } } /// /// Checks if the current access token is active. /// public bool IsAuthenticated() { return !string.IsNullOrEmpty(_accessToken) && DateTime.UtcNow < _tokenExpiry; } /// /// Discovers SObjects (entities) and their fields from Salesforce. /// /// Cancellation token /// A list of discovered entity information public async Task> DiscoverEntitiesAsync(CancellationToken cancellationToken = default) { if (!IsAuthenticated()) { Console.WriteLine("Error: Not authenticated to Salesforce. Cannot discover metadata."); return new List(); } var entities = new List(); 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(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(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; } /// /// Discovers a list of available SObjects from Salesforce without detailed field information. /// /// Cancellation token /// A list of discovered SObject summaries public async Task> DiscoverEntitySummariesAsync(CancellationToken cancellationToken = default) { if (!IsAuthenticated()) { Console.WriteLine("Error: Not authenticated to Salesforce. Cannot discover metadata."); return new List(); } var entities = new List(); 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(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(); } /// /// Discovers detailed information for a specific SObject including all its fields. /// /// The name of the SObject to get details for /// Cancellation token /// Detailed SObject information or null if not found public async Task 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(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; } /// /// Creates a new SObject in Salesforce. /// /// The name of the SObject to create (e.g., "Account", "Contact"). /// The data for the new SObject as key-value pairs. /// Cancellation token. /// The created entity data or null if creation failed. public override async Task?> CreateEntityAsync(string entityName, Dictionary 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>(responseContent); // Merge the original data with the creation result (which includes the new Id) if (creationResult != null) { var result = new Dictionary(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; } } // --- 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 SObjects { get; set; } = new List(); } 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 Fields { get; set; } = new List(); } 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; } } } }