Files
Data-Coupler/CredentialManager/Integration/DataConnectionHelper.cs
T
Alessio e70abcdcb1 [Feature] Supporto Visual FoxPro / dBase come sorgente dati (managed, sola lettura)
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>
2026-06-06 19:34:21 +02:00

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);
}
}