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>
219 lines
8.6 KiB
C#
219 lines
8.6 KiB
C#
using CredentialManager.Models;
|
|
using CredentialManager.Services;
|
|
|
|
namespace CredentialManager.Integration;
|
|
|
|
/// <summary>
|
|
/// Metodi di utilità per l'integrazione con DataConnection
|
|
/// </summary>
|
|
public static class DataConnectionHelper
|
|
{
|
|
/// <summary>
|
|
/// Converte una DatabaseCredential in una stringa di connessione
|
|
/// </summary>
|
|
/// <param name="credential">La credenziale database</param>
|
|
/// <returns>Stringa di connessione pronta per l'uso</returns>
|
|
public static string ToConnectionString(this DatabaseCredential credential)
|
|
{
|
|
return ConnectionStringBuilder.BuildConnectionString(credential);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Crea le opzioni per RestServiceOptions dal progetto DataConnection
|
|
/// </summary>
|
|
/// <param name="credential">La credenziale REST API</param>
|
|
/// <returns>Oggetto con le opzioni per REST service</returns>
|
|
public static object ToRestServiceOptions(this RestApiCredential credential)
|
|
{
|
|
return new
|
|
{
|
|
BaseUrl = credential.BaseUrl,
|
|
ApiKey = credential.ApiKey,
|
|
Username = credential.Username,
|
|
Password = credential.Password,
|
|
AuthToken = credential.AuthToken,
|
|
TimeoutSeconds = credential.TimeoutSeconds,
|
|
IgnoreSslErrors = credential.IgnoreSslErrors
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Crea le opzioni per DbManagerOptions dal progetto DataConnection
|
|
/// </summary>
|
|
/// <param name="credential">La credenziale database</param>
|
|
/// <returns>Oggetto con le opzioni per DB manager</returns>
|
|
public static object ToDbManagerOptions(this DatabaseCredential credential)
|
|
{
|
|
return new
|
|
{
|
|
ServerConnectionString = credential.ToConnectionString(),
|
|
DatabaseName = credential.DatabaseName,
|
|
DatabaseType = credential.DatabaseType.ToString(),
|
|
CommandTimeout = credential.CommandTimeout
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene la porta predefinita per un tipo di database
|
|
/// </summary>
|
|
/// <param name="databaseType">Tipo di database</param>
|
|
/// <returns>Porta predefinita</returns>
|
|
public static int GetDefaultPort(DatabaseType databaseType)
|
|
{
|
|
return databaseType switch
|
|
{
|
|
DatabaseType.SqlServer => 1433,
|
|
DatabaseType.MySql => 3306,
|
|
DatabaseType.PostgreSql => 5432,
|
|
DatabaseType.Oracle => 1521,
|
|
DatabaseType.DB2 => 50000,
|
|
DatabaseType.SapHana => 30015,
|
|
DatabaseType.Sqlite => 0, // Non applicabile
|
|
_ => 0
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Valida una credenziale database
|
|
/// </summary>
|
|
/// <param name="credential">La credenziale da validare</param>
|
|
/// <returns>Lista di errori di validazione (vuota se valida)</returns>
|
|
public static List<string> ValidateDatabaseCredential(DatabaseCredential credential)
|
|
{
|
|
var errors = new List<string>();
|
|
|
|
// Sorgenti file-based (percorso, senza host/utente/password/porta)
|
|
bool isFileBased = credential.DatabaseType == DatabaseType.Sqlite
|
|
|| credential.DatabaseType == DatabaseType.Foxpro;
|
|
|
|
if (string.IsNullOrWhiteSpace(credential.Name))
|
|
errors.Add("Il nome della credenziale è obbligatorio");
|
|
|
|
if (string.IsNullOrWhiteSpace(credential.Host) && !isFileBased)
|
|
errors.Add("L'host è obbligatorio per questo tipo di database");
|
|
|
|
if (string.IsNullOrWhiteSpace(credential.DatabaseName))
|
|
errors.Add(credential.DatabaseType == DatabaseType.Foxpro
|
|
? "Il percorso del database FoxPro (.dbc o cartella .dbf) è obbligatorio"
|
|
: "Il nome del database è obbligatorio");
|
|
|
|
if (string.IsNullOrWhiteSpace(credential.Username) && !isFileBased)
|
|
errors.Add("Il nome utente è obbligatorio per questo tipo di database");
|
|
|
|
if (string.IsNullOrWhiteSpace(credential.Password) && !isFileBased)
|
|
errors.Add("La password è obbligatoria per questo tipo di database");
|
|
|
|
if (credential.Port <= 0 && !isFileBased)
|
|
{
|
|
// Assegna porta predefinita se non specificata
|
|
credential.Port = GetDefaultPort(credential.DatabaseType);
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Valida una credenziale REST API
|
|
/// </summary>
|
|
/// <param name="credential">La credenziale da validare</param>
|
|
/// <returns>Lista di errori di validazione (vuota se valida)</returns>
|
|
public static List<string> ValidateRestApiCredential(RestApiCredential credential)
|
|
{
|
|
var errors = new List<string>();
|
|
|
|
if (string.IsNullOrWhiteSpace(credential.Name))
|
|
errors.Add("Il nome della credenziale è obbligatorio");
|
|
|
|
if (string.IsNullOrWhiteSpace(credential.BaseUrl))
|
|
errors.Add("L'URL base è obbligatorio");
|
|
else if (!Uri.TryCreate(credential.BaseUrl, UriKind.Absolute, out _))
|
|
errors.Add("L'URL base non è valido");
|
|
|
|
// Almeno uno tra ApiKey, Username/Password, o AuthToken deve essere specificato
|
|
var hasApiKey = !string.IsNullOrWhiteSpace(credential.ApiKey);
|
|
var hasUserPass = !string.IsNullOrWhiteSpace(credential.Username) && !string.IsNullOrWhiteSpace(credential.Password);
|
|
var hasAuthToken = !string.IsNullOrWhiteSpace(credential.AuthToken);
|
|
|
|
if (!hasApiKey && !hasUserPass && !hasAuthToken)
|
|
errors.Add("Deve essere specificato almeno un metodo di autenticazione (API Key, Username/Password, o Auth Token)");
|
|
|
|
return errors;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Estensioni per la gestione asincrona delle credenziali
|
|
/// </summary>
|
|
public static class CredentialServiceExtensions
|
|
{
|
|
/// <summary>
|
|
/// Ottiene una credenziale database validata
|
|
/// </summary>
|
|
/// <param name="service">Il servizio credenziali</param>
|
|
/// <param name="name">Nome della credenziale</param>
|
|
/// <returns>Credenziale validata o null se non trovata</returns>
|
|
public static async Task<DatabaseCredential?> GetValidatedDatabaseCredentialAsync(
|
|
this ICredentialService service, string name)
|
|
{
|
|
var credential = await service.GetDatabaseCredentialAsync(name);
|
|
if (credential == null) return null;
|
|
|
|
var errors = DataConnectionHelper.ValidateDatabaseCredential(credential);
|
|
if (errors.Any())
|
|
throw new ArgumentException($"Credenziale non valida: {string.Join(", ", errors)}");
|
|
|
|
return credential;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ottiene una credenziale REST API validata
|
|
/// </summary>
|
|
/// <param name="service">Il servizio credenziali</param>
|
|
/// <param name="name">Nome della credenziale</param>
|
|
/// <returns>Credenziale validata o null se non trovata</returns>
|
|
public static async Task<RestApiCredential?> GetValidatedRestApiCredentialAsync(
|
|
this ICredentialService service, string name)
|
|
{
|
|
var credential = await service.GetRestApiCredentialAsync(name);
|
|
if (credential == null) return null;
|
|
|
|
var errors = DataConnectionHelper.ValidateRestApiCredential(credential);
|
|
if (errors.Any())
|
|
throw new ArgumentException($"Credenziale non valida: {string.Join(", ", errors)}");
|
|
|
|
return credential;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Salva una credenziale database con validazione
|
|
/// </summary>
|
|
/// <param name="service">Il servizio credenziali</param>
|
|
/// <param name="credential">La credenziale da salvare</param>
|
|
/// <returns>ID della credenziale salvata</returns>
|
|
public static async Task<int> SaveValidatedDatabaseCredentialAsync(
|
|
this ICredentialService service, DatabaseCredential credential)
|
|
{
|
|
var errors = DataConnectionHelper.ValidateDatabaseCredential(credential);
|
|
if (errors.Any())
|
|
throw new ArgumentException($"Credenziale non valida: {string.Join(", ", errors)}");
|
|
|
|
return await service.SaveDatabaseCredentialAsync(credential);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Salva una credenziale REST API con validazione
|
|
/// </summary>
|
|
/// <param name="service">Il servizio credenziali</param>
|
|
/// <param name="credential">La credenziale da salvare</param>
|
|
/// <returns>ID della credenziale salvata</returns>
|
|
public static async Task<int> SaveValidatedRestApiCredentialAsync(
|
|
this ICredentialService service, RestApiCredential credential)
|
|
{
|
|
var errors = DataConnectionHelper.ValidateRestApiCredential(credential);
|
|
if (errors.Any())
|
|
throw new ArgumentException($"Credenziale non valida: {string.Join(", ", errors)}");
|
|
|
|
return await service.SaveRestApiCredentialAsync(credential);
|
|
}
|
|
}
|