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
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Models\" />
|
<Folder Include="DB\Models\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
namespace DataConnection.REST.Configuration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration options for a REST service client.
|
||||||
|
/// </summary>
|
||||||
|
public class RestServiceOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base URL of the REST service.
|
||||||
|
/// </summary>
|
||||||
|
public string? BaseUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// API Key, if required.
|
||||||
|
/// </summary>
|
||||||
|
public string? ApiKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Username for authentication, if required.
|
||||||
|
/// </summary>
|
||||||
|
public string? Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Password for authentication, if required.
|
||||||
|
/// </summary>
|
||||||
|
public string? Password { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Authentication token (e.g., Bearer token), if required.
|
||||||
|
/// </summary>
|
||||||
|
public string? AuthToken { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Timeout for requests in seconds.
|
||||||
|
/// </summary>
|
||||||
|
public int TimeoutSeconds { get; set; } = 100; // Default timeout
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to ignore SSL certificate errors.
|
||||||
|
/// Use with caution, especially in production environments.
|
||||||
|
/// </summary>
|
||||||
|
public bool IgnoreSslErrors { get; set; } = false;
|
||||||
|
|
||||||
|
// Add other relevant configuration properties (e.g., OAuth settings, specific headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Base implementation for a REST service client using HttpClient.
|
||||||
|
/// </summary>
|
||||||
|
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<T?> GetAsync<T>(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<T>(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<TResponse?> PostAsync<TRequest, TResponse>(string requestUri, TRequest payload, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _httpClient.PostAsJsonAsync(requestUri, payload, cancellationToken);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
return await response.Content.ReadFromJsonAsync<TResponse>(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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Client specific for SAP Business One Service Layer.
|
||||||
|
/// Handles session-based authentication and metadata discovery.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs into SAP Business One Service Layer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="companyDB">The Company Database name.</param>
|
||||||
|
/// <param name="userName">Service Layer username.</param>
|
||||||
|
/// <param name="password">Service Layer password.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>True if login is successful, false otherwise.</returns>
|
||||||
|
public async Task<bool> 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<SapB1SessionInfo>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs out from SAP Business One Service Layer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>Task representing the asynchronous operation.</returns>
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if the current session is active based on timeout.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSessionActive()
|
||||||
|
{
|
||||||
|
return !string.IsNullOrEmpty(_b1SessionId) && DateTime.UtcNow < _sessionTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Discovers entities and their properties from the SAP B1 Service Layer metadata.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>A list of discovered entity information.</returns>
|
||||||
|
public async Task<List<RestEntityInfo>> 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<RestEntityInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadataUri = "/b1s/v2/$metadata"; // Adjust version (v1/v2) if necessary
|
||||||
|
var entities = new List<RestEntityInfo>();
|
||||||
|
|
||||||
|
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<string>();
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using DataConnection.REST.Models;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DataConnection.REST.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for discovering metadata (entities, properties) from a REST service.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRestMetadataDiscovery
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Discovers entities and their properties from the REST service metadata.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>A list of discovered entity information.</returns>
|
||||||
|
Task<List<RestEntityInfo>> DiscoverEntitiesAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DataConnection.REST.Interfaces
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for a generic REST service client.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRestServiceClient
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a GET request to the specified URI.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of the object to deserialize the response content to.</typeparam>
|
||||||
|
/// <param name="requestUri">The URI the request is sent to.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The deserialized response content.</returns>
|
||||||
|
Task<T?> GetAsync<T>(string requestUri, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sends a POST request to the specified URI.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TRequest">The type of the request object.</typeparam>
|
||||||
|
/// <typeparam name="TResponse">The type of the object to deserialize the response content to.</typeparam>
|
||||||
|
/// <param name="requestUri">The URI the request is sent to.</param>
|
||||||
|
/// <param name="payload">The HTTP request content sent to the server.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
|
/// <returns>The deserialized response content.</returns>
|
||||||
|
Task<TResponse?> PostAsync<TRequest, TResponse>(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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace DataConnection.REST.Models
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents information about a discovered REST entity (resource).
|
||||||
|
/// </summary>
|
||||||
|
public class RestEntityInfo
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public List<RestPropertyInfo> Properties { get; set; } = new List<RestPropertyInfo>();
|
||||||
|
// Add other relevant info like Key properties if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents information about a property of a discovered REST entity.
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|
||||||
|
<h3>REST Service Discovery</h3>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="serviceType" class="form-label">Service Type:</label>
|
||||||
|
<select id="serviceType" class="form-select" @bind="SelectedServiceType">
|
||||||
|
<option value="">-- Select Service --</option>
|
||||||
|
<option value="SAPB1">SAP Business One Service Layer</option>
|
||||||
|
@* Add other service types here later *@
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (SelectedServiceType == "SAPB1")
|
||||||
|
{
|
||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">SAP Business One Connection Details</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="baseUrl" class="form-label">Service Layer Base URL:</label>
|
||||||
|
<input type="text" id="baseUrl" class="form-control" @bind="SapB1Options.BaseUrl" placeholder="e.g., https://sap-server:50000" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="companyDb" class="form-label">Company DB:</label>
|
||||||
|
<input type="text" id="companyDb" class="form-control" @bind="SapB1Options.CompanyDb" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">Username:</label>
|
||||||
|
<input type="text" id="username" class="form-control" @bind="SapB1Options.Username" />
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label">Password:</label>
|
||||||
|
<input type="password" id="password" class="form-control" @bind="SapB1Options.Password" />
|
||||||
|
</div>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="ignoreSsl" @bind="SapB1Options.IgnoreSslErrors">
|
||||||
|
<label class="form-check-label" for="ignoreSsl">
|
||||||
|
Ignore SSL Errors (Use with caution)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-primary" @onclick="HandleConnectClick" disabled="IsLoading">
|
||||||
|
@if (IsLoading)
|
||||||
|
{
|
||||||
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||||
|
<span> Connecting...</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>Connect and Discover Entities</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(StatusMessage))
|
||||||
|
{
|
||||||
|
<div class="alert @StatusMessageClass mt-3" role="alert">
|
||||||
|
@StatusMessage
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (DiscoveredEntities.Any())
|
||||||
|
{
|
||||||
|
<h4 class="mt-4">Discovered Entities</h4>
|
||||||
|
<div style="max-height: 500px; overflow-y: auto;">
|
||||||
|
<ul class="list-group">
|
||||||
|
@foreach (var entity in DiscoveredEntities.OrderBy(e => e.Name))
|
||||||
|
{
|
||||||
|
<li class="list-group-item">
|
||||||
|
<h5>@entity.Name</h5>
|
||||||
|
<ul class="list-unstyled ms-3">
|
||||||
|
@foreach (var prop in entity.Properties.OrderBy(p => p.Name))
|
||||||
|
{
|
||||||
|
<li>
|
||||||
|
<code>@prop.Name</code> (@prop.Type) @(prop.IsKey ? "[Key]" : "")
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private string SelectedServiceType { get; set; } = "";
|
||||||
|
private SapB1ConnectionOptions SapB1Options { get; set; } = new SapB1ConnectionOptions();
|
||||||
|
private List<RestEntityInfo> DiscoveredEntities { get; set; } = new List<RestEntityInfo>();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,9 @@ builder.Services.AddScoped<DatabaseConnectionService>();
|
|||||||
builder.Services.AddSingleton<IDatabaseDiscovery, SqlServerDatabaseDiscovery>();
|
builder.Services.AddSingleton<IDatabaseDiscovery, SqlServerDatabaseDiscovery>();
|
||||||
builder.Services.AddSingleton<DbManagerOptions>();
|
builder.Services.AddSingleton<DbManagerOptions>();
|
||||||
|
|
||||||
|
// Register IHttpClientFactory
|
||||||
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,11 @@
|
|||||||
<span class="oi oi-list" aria-hidden="true"></span> Struttura Database
|
<span class="oi oi-list" aria-hidden="true"></span> Struttura Database
|
||||||
</NavLink>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav-item px-3">
|
||||||
|
<NavLink class="nav-link" href="rest-discovery">
|
||||||
|
<span class="oi oi-cloud" aria-hidden="true"></span> REST Discovery
|
||||||
|
</NavLink>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user