e70abcdcb1
Aggiunge un connettore completamente managed per database Visual FoxPro e dBase, utilizzabile come sorgente dati in tutti i flussi dell'applicazione (discovery manuale, esecuzione schedulata, hash/change-detection, backup/restore). ## Motivazione Il provider OLE DB ufficiale (VFPOLEDB.1) è solo a 32-bit, deprecato e richiede installazione separata. Il connettore managed usa DbfDataReader 1.0.0 (MIT), legge direttamente i file .dbf/.fpt/.dbc a 64-bit senza dipendenze esterne e con streaming efficiente (tabelle da centinaia di MB con uso di memoria contenuto). ## Nuovi file - DataConnection/DB/FoxPro/FoxProConnectionInfo.cs Classe sealed con le informazioni di connessione risolte (cartella, percorso .dbc, encoding, flag IncludeDeleted). Proprietà di convenienza IsContainer e DisplayName. - DataConnection/DB/FoxPro/FoxProReader.cs Helper statico che registra CodePagesEncodingProvider, risolve la connection string (path diretto, stile OLE DB con Provider=vfpoledb, chiave=valore), verifica l'accesso al filesystem, legge il catalogo .dbc (il file .dbc è esso stesso un DBF; filtro su OBJECTTYPE='Table' + cross-check esistenza fisica .dbf), enumera le tabelle free, mappa i tipi VFP (Character, Memo, Numeric, Float, Double, Integer, Currency, Date, DateTime, Logical, General) in descrizioni leggibili, esegue streaming dei record via DbfDataReader, auto-rileva l'encoding dall'header DBF (language driver byte) con fallback a code page 1252, e analizza query SELECT [TOP n] * FROM tabella. - DataConnection/DB/FoxProDatabaseManager.cs Implementa IDatabaseManager. Lettura: TestConnectionAsync, GetTableNamesAsync, GetTableSchemaAsync, GetDatabaseSchemaAsync, GetAllRecordsAsync, ExecuteRawQueryAsync, GetPrimaryKeyFieldAsync (ritorna null, selezione manuale chiave), GetAvailableDatabasesAsync, ChangeDatabaseAsync (no-op per sorgenti file-based). Scrittura: tutti i metodi di modifica (Insert/Update/Delete/Upsert/BulkInsert/ ExecuteCommand/ExecuteNonQuery) lanciano NotSupportedException con messaggio esplicito. - DataConnection/DB/EF/SchemaProviders/FoxProSchemaProvider.cs Implementa IDatabaseSchemaProvider delegando a FoxProReader. Gestione errori per tabella in GetDatabaseSchemaAsync (una tabella non apribile non blocca la discovery). - FOXPRO_CONNECTION.md Guida utente: passo-passo per creare la credenziale, formato connection string, tabella tipi VFP supportati, note su sola lettura e limitazioni query, tabella risoluzione problemi (caratteri accentati, tabelle mancanti, ecc.). ## File modificati - DataConnection/DataConnection.csproj Aggiunto DbfDataReader 1.0.0 (porta transitivamente System.Text.Encoding.CodePages >= 10.0.3; rimossa dipendenza esplicita alla versione 9.0.3 che causava NU1605). - DataConnection/DB/Enums/DatabaseType.cs Aggiunto valore Foxpro in fondo all'enum (preserva valori già persistiti). - CredentialManager/Models/CredentialModels.cs Aggiunto DatabaseType.Foxpro all'enum e metodo BuildFoxproConnectionString che genera "Data Source=percorso[;CodePage=n][;IncludeDeleted=true]". - DataConnection/CredentialManagement/Models/CredentialExtensions.cs Mappatura bidirezionale Foxpro tra enum CredentialManager e DataConnection. - DataConnection/CredentialManagement/Services/DataConnectionCredentialService.cs Aggiunto TestFoxproConnection: verifica percorso accessibile, legge nomi tabelle, restituisce messaggio con conteggio tabelle, encoding rilevato e nota sola lettura. - DataConnection/DB/EF/DatabaseSchemaProviderFactory.cs Caso Foxpro => new FoxProSchemaProvider(). - Data_Coupler/Services/DataConnectionFactory.cs Branch Foxpro => new FoxProDatabaseManager(connectionString). - Data_Coupler/Extensions/DataCoupler/DatabaseMethod.cs Aggiunto IsFoxproConnection() e caso Foxpro in CreateLimitedQuery (SELECT TOP n * FROM (query) AS subquery). - CredentialManager/Integration/DataConnectionHelper.cs ValidateDatabaseCredential: introdotto flag isFileBased (Sqlite || Foxpro) per esonerare host/utente/password/porta; messaggio di errore specifico per il campo percorso FoxPro. - Data_Coupler/Pages/CredentialManagement.razor Aggiunta opzione "Visual FoxPro (.dbc / .dbf)" nel selettore tipo database. Sezione di configurazione dedicata: banner sola lettura, campo percorso con esempi (.dbc e cartella), dropdown code page (Auto/1252/1250/1251/850/437/65001), checkbox record cancellati, pannello tipi supportati, anteprima connection string. Fix validazione form test-connessione: branch Foxpro che verifica solo DatabaseName (non Host/Username/Password), eliminando il falso errore "Il campo Host è obbligatorio". Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
272 lines
13 KiB
C#
272 lines
13 KiB
C#
using CredentialManager.Models;
|
|
using DataConnection.EF;
|
|
using DataConnection.Interfaces;
|
|
using DataConnection.REST.Interfaces;
|
|
using DataConnection.REST.Implementations;
|
|
using DataConnection.REST.Configuration;
|
|
using DataConnection.CredentialManagement.Interfaces;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using System.Security.Cryptography;
|
|
using System.Text;
|
|
|
|
namespace Data_Coupler.Services
|
|
{
|
|
/// <summary>
|
|
/// Classe per tracciare i client REST cached con le loro credenziali
|
|
/// </summary>
|
|
internal class CachedRestClient
|
|
{
|
|
public IRestServiceClient Client { get; set; } = null!;
|
|
public string CredentialsHash { get; set; } = string.Empty;
|
|
public DateTime CachedAt { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Factory service per creare istanze di DatabaseManager e RestClient
|
|
/// </summary>
|
|
public interface IDataConnectionFactory
|
|
{
|
|
/// <summary>
|
|
/// Crea un DatabaseManager per la credenziale specificata
|
|
/// </summary>
|
|
Task<IDatabaseManager> CreateDatabaseManagerAsync(string credentialName);
|
|
|
|
/// <summary>
|
|
/// Crea un RestServiceClient per la credenziale specificata
|
|
/// </summary>
|
|
Task<IRestServiceClient> CreateRestServiceClientAsync(string credentialName);
|
|
|
|
/// <summary>
|
|
/// Crea un RestMetadataDiscovery per la credenziale specificata
|
|
/// </summary>
|
|
Task<IRestMetadataDiscovery> CreateRestMetadataDiscoveryAsync(string credentialName);
|
|
|
|
/// <summary>
|
|
/// Pulisce la cache del client REST per una credenziale specifica
|
|
/// </summary>
|
|
void ClearRestClientCache(string credentialName);
|
|
/// <summary>
|
|
/// Pulisce tutta la cache dei client REST
|
|
/// </summary>
|
|
void ClearAllRestClientCache();
|
|
}
|
|
|
|
public class DataConnectionFactory : IDataConnectionFactory
|
|
{
|
|
private readonly IServiceProvider _serviceProvider;
|
|
private readonly ILogger<DataConnectionFactory> _logger;
|
|
private readonly IDataConnectionCredentialService _credentialService;
|
|
|
|
// Cache intelligente per client REST che mantiene lo stato di autenticazione ma verifica le credenziali
|
|
private readonly Dictionary<string, CachedRestClient> _restClientCache = new();
|
|
|
|
public DataConnectionFactory(IServiceProvider serviceProvider, ILogger<DataConnectionFactory> logger, IDataConnectionCredentialService credentialService)
|
|
{
|
|
_serviceProvider = serviceProvider;
|
|
_logger = logger;
|
|
_credentialService = credentialService;
|
|
}public async Task<IDatabaseManager> CreateDatabaseManagerAsync(string credentialName)
|
|
{
|
|
try
|
|
{
|
|
var credential = await _credentialService.GetDatabaseCredentialAsync(credentialName);
|
|
if (credential == null)
|
|
{
|
|
throw new ArgumentException($"Credenziale database '{credentialName}' non trovata");
|
|
}
|
|
// Per ODBC, usa OdbcDatabaseManager direttamente (EF Core non supporta ODBC)
|
|
if (credential.DatabaseType == DatabaseType.Odbc)
|
|
{
|
|
var connectionString = CredentialManager.Models.ConnectionStringBuilder.BuildConnectionString(credential);
|
|
_logger.LogInformation("Creando OdbcDatabaseManager con connection string per {CredentialName}", credentialName);
|
|
return new DataConnection.DB.OdbcDatabaseManager(connectionString);
|
|
}
|
|
|
|
// Per OLE DB, usa OleDbDatabaseManager direttamente (EF Core non supporta OLE DB)
|
|
if (credential.DatabaseType == DatabaseType.OleDb)
|
|
{
|
|
var connectionString = CredentialManager.Models.ConnectionStringBuilder.BuildConnectionString(credential);
|
|
_logger.LogInformation("Creando OleDbDatabaseManager con connection string per {CredentialName}", credentialName);
|
|
return new DataConnection.DB.OleDbDatabaseManager(connectionString);
|
|
}
|
|
|
|
// Per Visual FoxPro, usa FoxProDatabaseManager (lettura managed di .dbf/.fpt/.dbc, 64-bit)
|
|
if (credential.DatabaseType == DatabaseType.Foxpro)
|
|
{
|
|
var connectionString = CredentialManager.Models.ConnectionStringBuilder.BuildConnectionString(credential);
|
|
_logger.LogInformation("Creando FoxProDatabaseManager (sola lettura) per {CredentialName}", credentialName);
|
|
return new DataConnection.DB.FoxProDatabaseManager(connectionString);
|
|
}
|
|
|
|
// Per altri database, usa EFCoreDatabaseManager
|
|
var dbManagerOptions = await _credentialService.GetDbManagerOptionsAsync(credential.Name);
|
|
return new EFCoreDatabaseManager(dbManagerOptions);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Errore nella creazione del DatabaseManager per {CredentialName}", credentialName);
|
|
throw;
|
|
}
|
|
} public async Task<IRestServiceClient> CreateRestServiceClientAsync(string credentialName)
|
|
{
|
|
try
|
|
{
|
|
// SEMPRE recupera le credenziali fresh dal database per assicurare che siano aggiornate
|
|
var credential = await _credentialService.GetRestApiCredentialAsync(credentialName);
|
|
if (credential == null)
|
|
{
|
|
throw new ArgumentException($"Credenziale REST API '{credentialName}' non trovata");
|
|
}
|
|
|
|
// Calcola l'hash delle credenziali correnti
|
|
var currentCredentialsHash = CalculateCredentialsHash(credential);
|
|
|
|
// Controlla se abbiamo un client cached con le stesse credenziali
|
|
if (_restClientCache.TryGetValue(credentialName, out var cachedClient))
|
|
{
|
|
if (cachedClient.CredentialsHash == currentCredentialsHash)
|
|
{
|
|
_logger.LogInformation("Utilizzando client REST cached con credenziali verificate per {CredentialName}", credentialName);
|
|
return cachedClient.Client;
|
|
}
|
|
else
|
|
{
|
|
_logger.LogInformation("Credenziali cambiate, rimuovendo client cached per {CredentialName}", credentialName);
|
|
_restClientCache.Remove(credentialName);
|
|
}
|
|
} var options = new RestServiceOptions
|
|
{
|
|
BaseUrl = credential.BaseUrl,
|
|
Username = credential.Username,
|
|
Password = credential.Password,
|
|
TimeoutSeconds = credential.TimeoutSeconds,
|
|
IgnoreSslErrors = credential.IgnoreSslErrors
|
|
}; // Mapping specifico per tipo di servizio
|
|
switch (credential.ServiceType)
|
|
{
|
|
case RestServiceType.Salesforce:
|
|
// Per Salesforce usiamo i campi specifici ClientId e ClientSecret
|
|
options.ApiKey = credential.ClientId; // ClientId -> ApiKey
|
|
options.AuthToken = credential.ClientSecret; // ClientSecret -> AuthToken
|
|
options.SalesforceGrantType = credential.GrantType;
|
|
_logger.LogInformation("Salesforce mapping - GrantType: {GrantType}, ClientId: '{ClientId}', ClientSecret: {HasSecret}, Username: '{Username}', Password: {HasPassword}",
|
|
credential.GrantType,
|
|
credential.ClientId,
|
|
!string.IsNullOrEmpty(credential.ClientSecret),
|
|
credential.Username,
|
|
!string.IsNullOrEmpty(credential.Password));
|
|
break;
|
|
case RestServiceType.SapB1ServiceLayer:
|
|
// Per SAP B1 usiamo il CompanyDatabase come ApiKey
|
|
options.ApiKey = credential.CompanyDatabase;
|
|
options.AuthToken = credential.AuthToken;
|
|
_logger.LogInformation("SAP B1 mapping - CompanyDB: {CompanyDB}, Username: {Username}",
|
|
credential.CompanyDatabase, credential.Username);
|
|
break;
|
|
default:
|
|
// Per servizi generici usiamo ApiKey e AuthToken direttamente
|
|
options.ApiKey = credential.ApiKey;
|
|
options.AuthToken = credential.AuthToken;
|
|
_logger.LogInformation("Generic mapping - ApiKey: {ApiKey}, AuthToken: {HasToken}, Username: {Username}",
|
|
credential.ApiKey, !string.IsNullOrEmpty(credential.AuthToken), credential.Username);
|
|
break;
|
|
}
|
|
|
|
var client = credential.ServiceType switch
|
|
{
|
|
RestServiceType.SapB1ServiceLayer => new SapB1ServiceClient(options),
|
|
RestServiceType.Salesforce => CreateSalesforceClient(options),
|
|
RestServiceType.Generic => CreateGenericRestClient(options),
|
|
_ => throw new NotSupportedException($"Tipo di servizio REST non supportato: {credential.ServiceType}")
|
|
};
|
|
|
|
// Salva il client nella cache con l'hash delle credenziali
|
|
_restClientCache[credentialName] = new CachedRestClient
|
|
{
|
|
Client = client,
|
|
CredentialsHash = currentCredentialsHash,
|
|
CachedAt = DateTime.UtcNow
|
|
};
|
|
|
|
_logger.LogInformation("Client REST creato e cached con credenziali aggiornate per {CredentialName}", credentialName);
|
|
|
|
return client;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Errore nella creazione del RestServiceClient per {CredentialName}", credentialName);
|
|
throw;
|
|
}
|
|
} public async Task<IRestMetadataDiscovery> CreateRestMetadataDiscoveryAsync(string credentialName)
|
|
{
|
|
try
|
|
{
|
|
// Crea un nuovo client REST con credenziali aggiornate
|
|
var restClient = await CreateRestServiceClientAsync(credentialName);
|
|
|
|
// I service client già implementano IRestMetadataDiscovery
|
|
if (restClient is IRestMetadataDiscovery metadataDiscovery)
|
|
{
|
|
_logger.LogInformation("Utilizzando client REST per metadata discovery con credenziali aggiornate per {CredentialName}", credentialName);
|
|
return metadataDiscovery;
|
|
}
|
|
|
|
var credential = await _credentialService.GetRestApiCredentialAsync(credentialName);
|
|
throw new NotSupportedException($"Il servizio REST {credential?.ServiceType} non supporta il metadata discovery");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Errore nella creazione del RestMetadataDiscovery per {CredentialName}", credentialName);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private IRestServiceClient CreateGenericRestClient(RestServiceOptions options)
|
|
{
|
|
var httpClientFactory = _serviceProvider.GetRequiredService<IHttpClientFactory>();
|
|
var httpClient = httpClientFactory.CreateClient();
|
|
return new BaseRestServiceClient(httpClient, options);
|
|
}
|
|
|
|
private IRestServiceClient CreateSalesforceClient(RestServiceOptions options)
|
|
{
|
|
var httpClientFactory = _serviceProvider.GetRequiredService<IHttpClientFactory>();
|
|
var httpClient = httpClientFactory.CreateClient();
|
|
var loggerFactory = _serviceProvider.GetService<Microsoft.Extensions.Logging.ILoggerFactory>();
|
|
var sfLogger = loggerFactory?.CreateLogger<DataConnection.REST.Implementations.SalesforceServiceClient>();
|
|
return new DataConnection.REST.Implementations.SalesforceServiceClient(httpClient, options, sfLogger);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calcola un hash delle credenziali per rilevare cambiamenti
|
|
/// </summary>
|
|
private string CalculateCredentialsHash(RestApiCredential credential)
|
|
{
|
|
var credentialString = $"{credential.BaseUrl}|{credential.Username}|{credential.Password}|{credential.ApiKey}|{credential.AuthToken}|{credential.ClientId}|{credential.ClientSecret}|{credential.CompanyDatabase}";
|
|
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(credentialString));
|
|
return Convert.ToBase64String(bytes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pulisce la cache del client REST per una credenziale specifica.
|
|
/// </summary>
|
|
public void ClearRestClientCache(string credentialName)
|
|
{
|
|
if (_restClientCache.Remove(credentialName))
|
|
{
|
|
_logger.LogInformation("Cache del client REST rimossa per {CredentialName}", credentialName);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pulisce tutta la cache dei client REST.
|
|
/// </summary>
|
|
public void ClearAllRestClientCache()
|
|
{
|
|
_restClientCache.Clear();
|
|
_logger.LogInformation("Cache di tutti i client REST pulita");
|
|
}
|
|
}
|
|
}
|