From 7346db3b633cfcf47abbef60f753e7af7b0e644e Mon Sep 17 00:00:00 2001 From: Alessio Dal Santo Date: Tue, 29 Apr 2025 00:16:03 +0200 Subject: [PATCH] feat: Implement ExistingDatabaseContext for managing existing databases with customizable naming strategies and auto-discovery of entities feat: Add SqlServerSchemaProvider for extracting database schema information from SQL Server feat: Introduce DatabaseType and NamingStrategy enums for better database management and naming conventions feat: Create IDatabaseDiscovery and IDatabaseManager interfaces for database operations and metadata retrieval feat: Develop REST service client architecture with BaseRestServiceClient and SAP Business One specific implementation feat: Implement REST service discovery page with UI for connecting to SAP Business One Service Layer and displaying discovered entities --- .../SqlServerDatabaseDiscovery.cs | 0 .../EF/DatabaseSchemaProviderFactory.cs | 0 .../{ => DB}/EF/DbManagerOptions.cs | 0 .../{ => DB}/EF/EFCoreDatabaseManager.cs | 0 .../{ => DB}/EF/ExistingDatabaseContext.cs | 0 .../{ => DB}/EF/ExistingDatabaseExample.cs | 0 .../SqlServerSchemaProvider.cs | 0 DataConnection/{ => DB}/Enums/DatabaseType.cs | 0 .../{ => DB}/Enums/NamingStrategy.cs | 0 .../{ => DB}/Interfaces/IDatabaseDiscovery.cs | 0 .../{ => DB}/Interfaces/IDatabaseManager.cs | 0 .../Interfaces/IDatabaseSchemaProvider.cs | 0 DataConnection/DataConnection.csproj | 2 +- .../REST/Configuration/RestServiceOptions.cs | 46 +++ .../Implementations/BaseRestServiceClient.cs | 119 +++++++ .../Implementations/SapB1ServiceClient.cs | 310 ++++++++++++++++++ .../REST/Interfaces/IRestMetadataDiscovery.cs | 20 ++ .../REST/Interfaces/IRestServiceClient.cs | 33 ++ .../REST/Models/RestMetadataInfo.cs | 25 ++ Data_Coupler/Pages/RestDiscovery.razor | 196 +++++++++++ Data_Coupler/Program.cs | 3 + Data_Coupler/Shared/NavMenu.razor | 5 + 22 files changed, 758 insertions(+), 1 deletion(-) rename DataConnection/{ => DB}/EF/DatabaseDiscovery/SqlServerDatabaseDiscovery.cs (100%) rename DataConnection/{ => DB}/EF/DatabaseSchemaProviderFactory.cs (100%) rename DataConnection/{ => DB}/EF/DbManagerOptions.cs (100%) rename DataConnection/{ => DB}/EF/EFCoreDatabaseManager.cs (100%) rename DataConnection/{ => DB}/EF/ExistingDatabaseContext.cs (100%) rename DataConnection/{ => DB}/EF/ExistingDatabaseExample.cs (100%) rename DataConnection/{ => DB}/EF/SchemaProviders/SqlServerSchemaProvider.cs (100%) rename DataConnection/{ => DB}/Enums/DatabaseType.cs (100%) rename DataConnection/{ => DB}/Enums/NamingStrategy.cs (100%) rename DataConnection/{ => DB}/Interfaces/IDatabaseDiscovery.cs (100%) rename DataConnection/{ => DB}/Interfaces/IDatabaseManager.cs (100%) rename DataConnection/{ => DB}/Interfaces/IDatabaseSchemaProvider.cs (100%) create mode 100644 DataConnection/REST/Configuration/RestServiceOptions.cs create mode 100644 DataConnection/REST/Implementations/BaseRestServiceClient.cs create mode 100644 DataConnection/REST/Implementations/SapB1ServiceClient.cs create mode 100644 DataConnection/REST/Interfaces/IRestMetadataDiscovery.cs create mode 100644 DataConnection/REST/Interfaces/IRestServiceClient.cs create mode 100644 DataConnection/REST/Models/RestMetadataInfo.cs create mode 100644 Data_Coupler/Pages/RestDiscovery.razor diff --git a/DataConnection/EF/DatabaseDiscovery/SqlServerDatabaseDiscovery.cs b/DataConnection/DB/EF/DatabaseDiscovery/SqlServerDatabaseDiscovery.cs similarity index 100% rename from DataConnection/EF/DatabaseDiscovery/SqlServerDatabaseDiscovery.cs rename to DataConnection/DB/EF/DatabaseDiscovery/SqlServerDatabaseDiscovery.cs diff --git a/DataConnection/EF/DatabaseSchemaProviderFactory.cs b/DataConnection/DB/EF/DatabaseSchemaProviderFactory.cs similarity index 100% rename from DataConnection/EF/DatabaseSchemaProviderFactory.cs rename to DataConnection/DB/EF/DatabaseSchemaProviderFactory.cs diff --git a/DataConnection/EF/DbManagerOptions.cs b/DataConnection/DB/EF/DbManagerOptions.cs similarity index 100% rename from DataConnection/EF/DbManagerOptions.cs rename to DataConnection/DB/EF/DbManagerOptions.cs diff --git a/DataConnection/EF/EFCoreDatabaseManager.cs b/DataConnection/DB/EF/EFCoreDatabaseManager.cs similarity index 100% rename from DataConnection/EF/EFCoreDatabaseManager.cs rename to DataConnection/DB/EF/EFCoreDatabaseManager.cs diff --git a/DataConnection/EF/ExistingDatabaseContext.cs b/DataConnection/DB/EF/ExistingDatabaseContext.cs similarity index 100% rename from DataConnection/EF/ExistingDatabaseContext.cs rename to DataConnection/DB/EF/ExistingDatabaseContext.cs diff --git a/DataConnection/EF/ExistingDatabaseExample.cs b/DataConnection/DB/EF/ExistingDatabaseExample.cs similarity index 100% rename from DataConnection/EF/ExistingDatabaseExample.cs rename to DataConnection/DB/EF/ExistingDatabaseExample.cs diff --git a/DataConnection/EF/SchemaProviders/SqlServerSchemaProvider.cs b/DataConnection/DB/EF/SchemaProviders/SqlServerSchemaProvider.cs similarity index 100% rename from DataConnection/EF/SchemaProviders/SqlServerSchemaProvider.cs rename to DataConnection/DB/EF/SchemaProviders/SqlServerSchemaProvider.cs diff --git a/DataConnection/Enums/DatabaseType.cs b/DataConnection/DB/Enums/DatabaseType.cs similarity index 100% rename from DataConnection/Enums/DatabaseType.cs rename to DataConnection/DB/Enums/DatabaseType.cs diff --git a/DataConnection/Enums/NamingStrategy.cs b/DataConnection/DB/Enums/NamingStrategy.cs similarity index 100% rename from DataConnection/Enums/NamingStrategy.cs rename to DataConnection/DB/Enums/NamingStrategy.cs diff --git a/DataConnection/Interfaces/IDatabaseDiscovery.cs b/DataConnection/DB/Interfaces/IDatabaseDiscovery.cs similarity index 100% rename from DataConnection/Interfaces/IDatabaseDiscovery.cs rename to DataConnection/DB/Interfaces/IDatabaseDiscovery.cs diff --git a/DataConnection/Interfaces/IDatabaseManager.cs b/DataConnection/DB/Interfaces/IDatabaseManager.cs similarity index 100% rename from DataConnection/Interfaces/IDatabaseManager.cs rename to DataConnection/DB/Interfaces/IDatabaseManager.cs diff --git a/DataConnection/Interfaces/IDatabaseSchemaProvider.cs b/DataConnection/DB/Interfaces/IDatabaseSchemaProvider.cs similarity index 100% rename from DataConnection/Interfaces/IDatabaseSchemaProvider.cs rename to DataConnection/DB/Interfaces/IDatabaseSchemaProvider.cs diff --git a/DataConnection/DataConnection.csproj b/DataConnection/DataConnection.csproj index 7e48384..cf13a89 100644 --- a/DataConnection/DataConnection.csproj +++ b/DataConnection/DataConnection.csproj @@ -7,7 +7,7 @@ - + diff --git a/DataConnection/REST/Configuration/RestServiceOptions.cs b/DataConnection/REST/Configuration/RestServiceOptions.cs new file mode 100644 index 0000000..6413091 --- /dev/null +++ b/DataConnection/REST/Configuration/RestServiceOptions.cs @@ -0,0 +1,46 @@ +namespace DataConnection.REST.Configuration +{ + /// + /// Configuration options for a REST service client. + /// + public class RestServiceOptions + { + /// + /// Base URL of the REST service. + /// + public string? BaseUrl { get; set; } + + /// + /// API Key, if required. + /// + public string? ApiKey { get; set; } + + /// + /// Username for authentication, if required. + /// + public string? Username { get; set; } + + /// + /// Password for authentication, if required. + /// + public string? Password { get; set; } + + /// + /// Authentication token (e.g., Bearer token), if required. + /// + public string? AuthToken { get; set; } + + /// + /// Timeout for requests in seconds. + /// + public int TimeoutSeconds { get; set; } = 100; // Default timeout + + /// + /// Gets or sets a value indicating whether to ignore SSL certificate errors. + /// Use with caution, especially in production environments. + /// + public bool IgnoreSslErrors { get; set; } = false; + + // Add other relevant configuration properties (e.g., OAuth settings, specific headers) + } +} diff --git a/DataConnection/REST/Implementations/BaseRestServiceClient.cs b/DataConnection/REST/Implementations/BaseRestServiceClient.cs new file mode 100644 index 0000000..a4447da --- /dev/null +++ b/DataConnection/REST/Implementations/BaseRestServiceClient.cs @@ -0,0 +1,119 @@ +using DataConnection.REST.Configuration; +using DataConnection.REST.Interfaces; +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace DataConnection.REST.Implementations +{ + /// + /// Base implementation for a REST service client using HttpClient. + /// + public class BaseRestServiceClient : IRestServiceClient, IDisposable + { + protected readonly HttpClient _httpClient; + protected readonly RestServiceOptions _options; + + public BaseRestServiceClient(HttpClient httpClient, RestServiceOptions options) + { + _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + + ConfigureHttpClient(); + } + + protected virtual void ConfigureHttpClient() + { + if (!string.IsNullOrWhiteSpace(_options.BaseUrl)) + { + _httpClient.BaseAddress = new Uri(_options.BaseUrl); + } + + _httpClient.Timeout = TimeSpan.FromSeconds(_options.TimeoutSeconds); + _httpClient.DefaultRequestHeaders.Accept.Clear(); + _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + // Add authentication headers based on options + if (!string.IsNullOrWhiteSpace(_options.ApiKey)) + { + // Example: Add API key header (adjust header name as needed) + _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("X-API-KEY", _options.ApiKey); + } + else if (!string.IsNullOrWhiteSpace(_options.AuthToken)) + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _options.AuthToken); + } + else if (!string.IsNullOrWhiteSpace(_options.Username) && !string.IsNullOrWhiteSpace(_options.Password)) + { + var basicAuthValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_options.Username}:{_options.Password}")); + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", basicAuthValue); + } + // Add other authentication methods (e.g., OAuth) if necessary + } + + public virtual async Task GetAsync(string requestUri, CancellationToken cancellationToken = default) + { + try + { + var response = await _httpClient.GetAsync(requestUri, cancellationToken); + response.EnsureSuccessStatusCode(); // Throws exception for non-success status codes + return await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + } + catch (HttpRequestException ex) + { + // Log or handle specific HTTP request errors + Console.WriteLine($"HTTP Request Error: {ex.Message}"); + // Consider custom exception handling + throw; + } + catch (Exception ex) + { + // Log or handle other errors (e.g., deserialization) + Console.WriteLine($"Error during GET request: {ex.Message}"); + throw; + } + } + + public virtual async Task PostAsync(string requestUri, TRequest payload, CancellationToken cancellationToken = default) + { + try + { + var response = await _httpClient.PostAsJsonAsync(requestUri, payload, cancellationToken); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + } + catch (HttpRequestException ex) + { + Console.WriteLine($"HTTP Request Error: {ex.Message}"); + throw; + } + catch (Exception ex) + { + Console.WriteLine($"Error during POST request: {ex.Message}"); + throw; + } + } + + // Implement other methods (PUT, DELETE, etc.) similarly + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // HttpClient is typically managed by HttpClientFactory, + // but if created directly, dispose it here. + // _httpClient?.Dispose(); + } + } + } +} diff --git a/DataConnection/REST/Implementations/SapB1ServiceClient.cs b/DataConnection/REST/Implementations/SapB1ServiceClient.cs new file mode 100644 index 0000000..4ea9466 --- /dev/null +++ b/DataConnection/REST/Implementations/SapB1ServiceClient.cs @@ -0,0 +1,310 @@ +using DataConnection.REST.Configuration; +using DataConnection.REST.Interfaces; +using DataConnection.REST.Models; // Added for metadata models +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; // Added for XML parsing + +namespace DataConnection.REST.Implementations +{ + /// + /// Client specific for SAP Business One Service Layer. + /// Handles session-based authentication and metadata discovery. + /// + public class SapB1ServiceClient : BaseRestServiceClient, IRestMetadataDiscovery // Implement the interface + { + private CookieContainer _cookieContainer; + private HttpClientHandler _httpClientHandler; + private HttpClient _sapHttpClient; // Use a dedicated HttpClient with cookie handling + + // SAP B1 Specific Login Info + private string? _b1SessionId; + private string? _routeId; + private DateTime _sessionTimeout; + + // Consider making options specific to SAP B1 if needed + public SapB1ServiceClient(RestServiceOptions options) + : base(CreateConfiguredHttpClient(options, out var handler, out var container), options) + { + // Store handler and container for direct cookie access if needed + _httpClientHandler = handler; + _cookieContainer = container; + _sapHttpClient = _httpClient; // Use the base class's configured HttpClient + } + + private static HttpClient CreateConfiguredHttpClient(RestServiceOptions options, out HttpClientHandler handler, out CookieContainer container) + { + container = new CookieContainer(); + handler = new HttpClientHandler + { + CookieContainer = container, + UseCookies = true, + // Service Layer might use self-signed certificates in test environments + // Use cautiously in production! + // ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator + }; + + // Conditionally ignore SSL errors based on options + if (options.IgnoreSslErrors) + { + handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + // Log a warning when ignoring SSL errors + Console.WriteLine("Warning: Ignoring SSL certificate errors. Use only for trusted development environments."); + } + + var client = new HttpClient(handler); + + // Configure base address, timeout etc. from base class logic + // The base constructor will call ConfigureHttpClient which uses _options + return client; + } + + /// + /// Logs into SAP Business One Service Layer. + /// + /// The Company Database name. + /// Service Layer username. + /// Service Layer password. + /// Cancellation token. + /// True if login is successful, false otherwise. + public async Task LoginAsync(string companyDB, string userName, string password, CancellationToken cancellationToken = default) + { + var loginPayload = new { CompanyDB = companyDB, UserName = userName, Password = password }; + var loginUri = "/b1s/v2/Login"; // Assuming v1, adjust if needed + + try + { + // Use the internal HttpClient configured with CookieContainer + var response = await _sapHttpClient.PostAsJsonAsync(loginUri, loginPayload, cancellationToken); + + if (!response.IsSuccessStatusCode) + { + // Log error details + Console.WriteLine($"SAP B1 Login failed: {response.StatusCode}"); + var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); + Console.WriteLine($"Error details: {errorContent}"); + return false; + } + + // Extract session details from response (if needed directly) and cookies + var sessionInfo = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); + + // Cookies are automatically handled by HttpClientHandler and CookieContainer + // Optionally, store session details if needed for manual checks or info + if (sessionInfo != null) + { + _b1SessionId = GetCookieValue("B1SESSION"); + _routeId = GetCookieValue("ROUTEID"); // Important for sticky sessions in load-balanced environments + _sessionTimeout = DateTime.UtcNow.AddSeconds(sessionInfo.SessionTimeout); + + Console.WriteLine($"SAP B1 Login successful. Session expires around: {_sessionTimeout.ToLocalTime()}"); + return true; + } + + Console.WriteLine("SAP B1 Login response could not be parsed."); + return false; + } + catch (HttpRequestException ex) + { + Console.WriteLine($"HTTP Request Error during SAP B1 Login: {ex.Message}"); + return false; + } + catch (JsonException ex) + { + Console.WriteLine($"JSON Parsing Error during SAP B1 Login: {ex.Message}"); + return false; + } + catch (Exception ex) + { + Console.WriteLine($"Error during SAP B1 Login: {ex.Message}"); + return false; + } + } + + /// + /// Logs out from SAP Business One Service Layer. + /// + /// Cancellation token. + /// Task representing the asynchronous operation. + public async Task LogoutAsync(CancellationToken cancellationToken = default) + { + var logoutUri = "/b1s/v2/Logout"; + try + { + // Send POST request to Logout endpoint. Cookies are sent automatically. + var response = await _sapHttpClient.PostAsync(logoutUri, null, cancellationToken); + response.EnsureSuccessStatusCode(); // Check if logout was successful + + // Clear local session state + _b1SessionId = null; + _routeId = null; + // Optionally clear cookies from container if needed, though they become invalid + ClearCookies(); + Console.WriteLine("SAP B1 Logout successful."); + } + catch (HttpRequestException ex) + { + // Log or handle error, maybe session already expired + Console.WriteLine($"HTTP Request Error during SAP B1 Logout: {ex.Message}"); + } + catch (Exception ex) + { + Console.WriteLine($"Error during SAP B1 Logout: {ex.Message}"); + } + } + + /// + /// Checks if the current session is active based on timeout. + /// + public bool IsSessionActive() + { + return !string.IsNullOrEmpty(_b1SessionId) && DateTime.UtcNow < _sessionTimeout; + } + + /// + /// Discovers entities and their properties from the SAP B1 Service Layer metadata. + /// + /// Cancellation token. + /// A list of discovered entity information. + public async Task> DiscoverEntitiesAsync(CancellationToken cancellationToken = default) + { + // Ensure session is active before attempting discovery + if (!IsSessionActive()) + { + // Or attempt login? For now, throw exception or return empty list + Console.WriteLine("Error: Not logged into SAP B1 Service Layer. Cannot discover metadata."); + // Consider throwing a specific exception + return new List(); + } + + var metadataUri = "/b1s/v2/$metadata"; // Adjust version (v1/v2) if necessary + var entities = new List(); + + try + { + // Use the internal HttpClient which handles cookies + var response = await _sapHttpClient.GetAsync(metadataUri, cancellationToken); + response.EnsureSuccessStatusCode(); + + var xmlContent = await response.Content.ReadAsStringAsync(cancellationToken); + var edmx = XDocument.Parse(xmlContent); + + // Define XML namespaces (adjust if different in your specific $metadata) + XNamespace edm = "http://schemas.microsoft.com/ado/2009/11/edm"; + XNamespace edmxNs = "http://schemas.microsoft.com/ado/2007/06/edmx"; // Older namespace sometimes used + XNamespace defaultEdmNs = edmx.Root?.GetDefaultNamespace() ?? edm; // Get default namespace if present + XNamespace dataServicesNs = edmx.Root?.Element(edmxNs + "DataServices")?.GetDefaultNamespace() ?? defaultEdmNs; + + + // Find the Schema element (might be nested) + var schemaElement = edmx.Descendants(defaultEdmNs + "Schema").FirstOrDefault() ?? + edmx.Descendants(dataServicesNs + "Schema").FirstOrDefault(); + + if (schemaElement == null) + { + Console.WriteLine("Error: Could not find Schema element in $metadata response."); + return entities; + } + + // Find all EntityType elements within the Schema + foreach (var entityTypeElement in schemaElement.Elements(defaultEdmNs + "EntityType")) + { + var entityInfo = new RestEntityInfo + { + Name = entityTypeElement.Attribute("Name")?.Value ?? string.Empty + }; + + if (string.IsNullOrEmpty(entityInfo.Name)) continue; // Skip if name is missing + + // Find key properties for this entity type + var keyProperties = new HashSet(); + var keyElement = entityTypeElement.Element(defaultEdmNs + "Key"); + if (keyElement != null) + { + foreach (var propRef in keyElement.Elements(defaultEdmNs + "PropertyRef")) + { + var keyName = propRef.Attribute("Name")?.Value; + if (!string.IsNullOrEmpty(keyName)) keyProperties.Add(keyName); + } + } + + // Find properties for this entity type + foreach (var propertyElement in entityTypeElement.Elements(defaultEdmNs + "Property")) + { + var propName = propertyElement.Attribute("Name")?.Value; + if (string.IsNullOrEmpty(propName)) continue; + + var propInfo = new RestPropertyInfo + { + Name = propName, + Type = propertyElement.Attribute("Type")?.Value ?? string.Empty, + IsKey = keyProperties.Contains(propName) + // Add extraction for Nullable, MaxLength etc. if needed from attributes + }; + entityInfo.Properties.Add(propInfo); + } + + entities.Add(entityInfo); + } + } + catch (HttpRequestException ex) + { + Console.WriteLine($"HTTP Request Error during metadata discovery: {ex.Message}"); + // Handle error appropriately + } + catch (XmlException ex) + { + Console.WriteLine($"XML Parsing Error during metadata discovery: {ex.Message}"); + // Handle error appropriately + } + catch (Exception ex) + { + Console.WriteLine($"Error during metadata discovery: {ex.Message}"); + // Handle error appropriately + } + + return entities; + } + + + // Helper to get cookie value + private string? GetCookieValue(string cookieName) + { + if (_httpClient.BaseAddress == null) return null; + var cookies = _cookieContainer.GetCookies(_httpClient.BaseAddress); + return cookies[cookieName]?.Value; + } + + // Helper to clear cookies + private void ClearCookies() + { + if (_httpClient.BaseAddress == null) return; + var cookies = _cookieContainer.GetCookies(_httpClient.BaseAddress); + foreach (Cookie cookie in cookies) + { + cookie.Expired = true; + } + } + + + // Ensure logout on dispose? Optional, depends on desired lifecycle management. + // public override void Dispose() { ... LogoutAsync().Wait(); ... base.Dispose(); } + + // --- Nested class for deserializing Login response --- + private class SapB1SessionInfo + { + public string SessionId { get; set; } = string.Empty; + public string Version { get; set; } = string.Empty; + public int SessionTimeout { get; set; } // In seconds + } + } +} diff --git a/DataConnection/REST/Interfaces/IRestMetadataDiscovery.cs b/DataConnection/REST/Interfaces/IRestMetadataDiscovery.cs new file mode 100644 index 0000000..60cc17c --- /dev/null +++ b/DataConnection/REST/Interfaces/IRestMetadataDiscovery.cs @@ -0,0 +1,20 @@ +using DataConnection.REST.Models; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace DataConnection.REST.Interfaces +{ + /// + /// Interface for discovering metadata (entities, properties) from a REST service. + /// + public interface IRestMetadataDiscovery + { + /// + /// Discovers entities and their properties from the REST service metadata. + /// + /// Cancellation token. + /// A list of discovered entity information. + Task> DiscoverEntitiesAsync(CancellationToken cancellationToken = default); + } +} diff --git a/DataConnection/REST/Interfaces/IRestServiceClient.cs b/DataConnection/REST/Interfaces/IRestServiceClient.cs new file mode 100644 index 0000000..1d5674a --- /dev/null +++ b/DataConnection/REST/Interfaces/IRestServiceClient.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; + +namespace DataConnection.REST.Interfaces +{ + /// + /// Interface for a generic REST service client. + /// + public interface IRestServiceClient + { + /// + /// Sends a GET request to the specified URI. + /// + /// The type of the object to deserialize the response content to. + /// The URI the request is sent to. + /// Cancellation token. + /// The deserialized response content. + Task GetAsync(string requestUri, CancellationToken cancellationToken = default); + + /// + /// Sends a POST request to the specified URI. + /// + /// The type of the request object. + /// The type of the object to deserialize the response content to. + /// The URI the request is sent to. + /// The HTTP request content sent to the server. + /// Cancellation token. + /// The deserialized response content. + Task PostAsync(string requestUri, TRequest payload, CancellationToken cancellationToken = default); + + // Add other methods as needed (PUT, DELETE, PATCH, etc.) + // Consider adding methods for handling raw HttpResponseMessage or string responses + } +} diff --git a/DataConnection/REST/Models/RestMetadataInfo.cs b/DataConnection/REST/Models/RestMetadataInfo.cs new file mode 100644 index 0000000..9c18a00 --- /dev/null +++ b/DataConnection/REST/Models/RestMetadataInfo.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace DataConnection.REST.Models +{ + /// + /// Represents information about a discovered REST entity (resource). + /// + public class RestEntityInfo + { + public string Name { get; set; } = string.Empty; + public List Properties { get; set; } = new List(); + // Add other relevant info like Key properties if needed + } + + /// + /// Represents information about a property of a discovered REST entity. + /// + public class RestPropertyInfo + { + public string Name { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; // Type as defined in the metadata (e.g., Edm.String, Edm.Int32) + public bool IsKey { get; set; } = false; + // Add other relevant info like Nullable, MaxLength etc. if needed + } +} diff --git a/Data_Coupler/Pages/RestDiscovery.razor b/Data_Coupler/Pages/RestDiscovery.razor new file mode 100644 index 0000000..db5fd78 --- /dev/null +++ b/Data_Coupler/Pages/RestDiscovery.razor @@ -0,0 +1,196 @@ +@page "/rest-discovery" +@using DataConnection.REST.Configuration +@using DataConnection.REST.Implementations +@using DataConnection.REST.Models +@using System.Net.Http +@inject IHttpClientFactory HttpClientFactory + +

REST Service Discovery

+ +
+ + +
+ +@if (SelectedServiceType == "SAPB1") +{ +
+
SAP Business One Connection Details
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+} + +@if (!string.IsNullOrEmpty(StatusMessage)) +{ + +} + +@if (DiscoveredEntities.Any()) +{ +

Discovered Entities

+
+
    + @foreach (var entity in DiscoveredEntities.OrderBy(e => e.Name)) + { +
  • +
    @entity.Name
    +
      + @foreach (var prop in entity.Properties.OrderBy(p => p.Name)) + { +
    • + @prop.Name (@prop.Type) @(prop.IsKey ? "[Key]" : "") +
    • + } +
    +
  • + } +
+
+} + +@code { + private string SelectedServiceType { get; set; } = ""; + private SapB1ConnectionOptions SapB1Options { get; set; } = new SapB1ConnectionOptions(); + private List DiscoveredEntities { get; set; } = new List(); + private bool IsLoading { get; set; } = false; + private string? StatusMessage { get; set; } + private string StatusMessageClass => !string.IsNullOrEmpty(StatusMessage) && StatusMessage.StartsWith("Error") ? "alert-danger" : "alert-success"; + + // Helper class to hold SAP B1 specific inputs + private class SapB1ConnectionOptions + { + public string? BaseUrl { get; set; } + public string? CompanyDb { get; set; } + public string? Username { get; set; } + public string? Password { get; set; } + public bool IgnoreSslErrors { get; set; } + } + + private async Task HandleConnectClick() + { + IsLoading = true; + StatusMessage = null; + DiscoveredEntities.Clear(); + StateHasChanged(); // Update UI to show loading + + if (SelectedServiceType == "SAPB1") + { + if (string.IsNullOrWhiteSpace(SapB1Options.BaseUrl) || + string.IsNullOrWhiteSpace(SapB1Options.CompanyDb) || + string.IsNullOrWhiteSpace(SapB1Options.Username) || + string.IsNullOrWhiteSpace(SapB1Options.Password)) + { + StatusMessage = "Error: Please fill in all SAP Business One connection details."; + IsLoading = false; + StateHasChanged(); + return; + } + + try + { + var restOptions = new RestServiceOptions + { + BaseUrl = SapB1Options.BaseUrl, + IgnoreSslErrors = SapB1Options.IgnoreSslErrors + // Username/Password are not set here as SapB1ServiceClient handles them in LoginAsync + }; + + // SapB1ServiceClient manages its own HttpClient internally for cookie handling + // We don't inject HttpClient directly into it via constructor in this setup. + // The BaseRestServiceClient constructor receives an HttpClient, but SapB1ServiceClient + // creates its own specific one in CreateConfiguredHttpClient. + // We pass the factory-created client to the base, but the SapB1 specific logic uses its own. + // This seems slightly complex, might need refactoring later, but follows current implementation. + + // Let's simplify: Create the client directly here for now. + var client = new SapB1ServiceClient(restOptions); + + StatusMessage = "Attempting login..."; + StateHasChanged(); + + bool loggedIn = await client.LoginAsync(SapB1Options.CompanyDb, SapB1Options.Username, SapB1Options.Password); + + if (loggedIn) + { + StatusMessage = "Login successful. Discovering entities..."; + StateHasChanged(); + + DiscoveredEntities = await client.DiscoverEntitiesAsync(); + + if (DiscoveredEntities.Any()) + { + StatusMessage = $"Discovery complete. Found {DiscoveredEntities.Count} entities."; + } + else + { + StatusMessage = "Login successful, but failed to discover entities or no entities found."; + } + + // Optional: Logout after discovery if desired + // await client.LogoutAsync(); + } + else + { + StatusMessage = "Error: SAP B1 Login failed. Check credentials and Service Layer status."; + } + } + catch (Exception ex) + { + // Log the full exception details somewhere appropriate + Console.WriteLine($"Error during SAP B1 connection/discovery: {ex}"); + StatusMessage = $"Error: An exception occurred: {ex.Message}"; + } + finally + { + IsLoading = false; + StateHasChanged(); // Update UI after completion/error + } + } + else + { + StatusMessage = "Error: Please select a service type."; + IsLoading = false; + StateHasChanged(); + } + } +} diff --git a/Data_Coupler/Program.cs b/Data_Coupler/Program.cs index f22e0a6..34fab0c 100644 --- a/Data_Coupler/Program.cs +++ b/Data_Coupler/Program.cs @@ -23,6 +23,9 @@ builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +// Register IHttpClientFactory +builder.Services.AddHttpClient(); + var app = builder.Build(); diff --git a/Data_Coupler/Shared/NavMenu.razor b/Data_Coupler/Shared/NavMenu.razor index a41290d..02aeaed 100644 --- a/Data_Coupler/Shared/NavMenu.razor +++ b/Data_Coupler/Shared/NavMenu.razor @@ -34,6 +34,11 @@ Struttura Database +