Files
Data-Coupler/DataConnection/DB/FoxProDatabaseManager.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

138 lines
5.6 KiB
C#

using System.Linq.Expressions;
using DataConnection.DB.FoxPro;
using DataConnection.Interfaces;
namespace DataConnection.DB;
/// <summary>
/// Database manager per sorgenti <b>Visual FoxPro / dBase</b> in modalità completamente
/// <b>managed</b>: legge direttamente i file <c>.dbf</c>/<c>.fpt</c>/<c>.dbc</c> tramite la
/// libreria <c>DbfDataReader</c>, senza alcun provider OLE DB/ODBC e funzionando a 64-bit.
///
/// <para><b>Sola lettura</b>: FoxPro è supportato come <i>sorgente</i> dati. Le operazioni di
/// scrittura lanciano <see cref="NotSupportedException"/> — scrivere nei <c>.dbf</c> rischierebbe
/// di corrompere indici <c>.cdx</c> e memo <c>.fpt</c> dei gestionali legacy.</para>
///
/// La "connection string" è semplicemente un percorso: un file <c>.dbc</c> (container) oppure
/// una cartella di tabelle libere <c>.dbf</c>. Vedi <see cref="FoxProReader.Resolve"/>.
/// </summary>
public class FoxProDatabaseManager : IDatabaseManager
{
private const string ReadOnlyMessage =
"Visual FoxPro è supportato in sola lettura (sorgente dati). La scrittura sui file .dbf non è disponibile.";
private readonly FoxProConnectionInfo _info;
public FoxProDatabaseManager(string connectionString)
{
_info = FoxProReader.Resolve(connectionString);
}
public async Task<bool> TestConnectionAsync()
{
try
{
await Task.Run(() =>
{
FoxProReader.EnsureReachable(_info);
// Forza la lettura dell'elenco tabelle: conferma che cartella/.dbc siano leggibili.
_ = FoxProReader.GetTableNames(_info);
});
return true;
}
catch
{
return false;
}
}
public Task<IEnumerable<string>> GetTableNamesAsync()
=> Task.Run(() => (IEnumerable<string>)FoxProReader.GetTableNames(_info));
public Task<IEnumerable<DbColumnInfo>> GetTableSchemaAsync(string tableName)
=> Task.Run(() => (IEnumerable<DbColumnInfo>)FoxProReader.GetTableColumns(_info, tableName));
public Task<IDictionary<string, IEnumerable<DbColumnInfo>>> GetDatabaseSchemaAsync()
{
return Task.Run(() =>
{
var result = new Dictionary<string, IEnumerable<DbColumnInfo>>(StringComparer.OrdinalIgnoreCase);
foreach (var table in FoxProReader.GetTableNames(_info))
{
try
{
var columns = FoxProReader.GetTableColumns(_info, table);
if (columns.Count > 0)
result[table] = columns;
}
catch (Exception ex)
{
Console.WriteLine($"[FoxPro] Schema non leggibile per {table}: {ex.Message}");
}
}
return (IDictionary<string, IEnumerable<DbColumnInfo>>)result;
});
}
public Task<IEnumerable<Dictionary<string, object>>> GetAllRecordsAsync(string tableName)
=> Task.Run(() => (IEnumerable<Dictionary<string, object>>)FoxProReader.ReadRecords(_info, tableName).ToList());
public Task<List<Dictionary<string, object>>> ExecuteRawQueryAsync(string sql, string databaseName = "", params object[] parameters)
{
return Task.Run(() =>
{
var parsed = FoxProReader.ParseSimpleSelect(sql);
if (parsed == null)
throw new NotSupportedException(
"Per le sorgenti FoxPro è supportato solo 'SELECT [TOP n] * FROM <tabella>'. " +
"Impossibile interpretare la query: " + sql);
var (table, top) = parsed.Value;
return FoxProReader.ReadRecords(_info, table, top).ToList();
});
}
public Task<string?> GetPrimaryKeyFieldAsync(string tableName)
// Le tabelle .dbf non espongono una PK affidabile: la chiave si seleziona nel coupler.
=> Task.FromResult<string?>(null);
public Task<List<string>> GetAvailableDatabasesAsync()
=> Task.FromResult(new List<string> { _info.DisplayName });
public Task ChangeDatabaseAsync(string databaseName)
{
// Sorgente file-based: il database è definito dal percorso. Nessun cambio a runtime.
return Task.CompletedTask;
}
// ===== Operazioni non supportate (sola lettura) =====
public Task<int> ExecuteCommandAsync(string sql, params object[] parameters)
=> throw new NotSupportedException(ReadOnlyMessage);
public Task<bool> UpsertRecordAsync(string tableName, string keyField, object? keyValue, Dictionary<string, object?> record)
=> throw new NotSupportedException(ReadOnlyMessage);
// ===== Operazioni LINQ/tipizzate non applicabili a una sorgente DBF =====
public Task<IEnumerable<T>> GetAsync<T>(
Expression<Func<T, bool>>? filter = null,
Func<IQueryable<T>, IOrderedQueryable<T>>? orderBy = null,
string includeProperties = "",
int? skip = null,
int? take = null) where T : class
=> throw new NotSupportedException("GetAsync<T> con LINQ non è supportato per FoxPro. Usare GetAllRecordsAsync.");
public Task<T?> GetByIdAsync<T>(object id) where T : class
=> throw new NotSupportedException("GetByIdAsync<T> non è supportato per FoxPro.");
public Task<IEnumerable<T>> ExecuteQueryAsync<T>(string sql, params object[] parameters) where T : class
=> throw new NotSupportedException("ExecuteQueryAsync<T> tipizzato non è supportato per FoxPro. Usare ExecuteRawQueryAsync.");
public void Dispose()
{
// Nessuna risorsa persistente: ogni operazione apre/chiude i file.
GC.SuppressFinalize(this);
}
}