[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>
This commit is contained in:
2026-06-06 19:34:21 +02:00
parent 11ff67f24d
commit e70abcdcb1
15 changed files with 1016 additions and 9 deletions
@@ -82,22 +82,28 @@ public static class DataConnectionHelper
{ {
var errors = new List<string>(); 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)) if (string.IsNullOrWhiteSpace(credential.Name))
errors.Add("Il nome della credenziale è obbligatorio"); errors.Add("Il nome della credenziale è obbligatorio");
if (string.IsNullOrWhiteSpace(credential.Host) && credential.DatabaseType != DatabaseType.Sqlite) if (string.IsNullOrWhiteSpace(credential.Host) && !isFileBased)
errors.Add("L'host è obbligatorio per questo tipo di database"); errors.Add("L'host è obbligatorio per questo tipo di database");
if (string.IsNullOrWhiteSpace(credential.DatabaseName)) if (string.IsNullOrWhiteSpace(credential.DatabaseName))
errors.Add("Il nome del database è obbligatorio"); 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) && credential.DatabaseType != DatabaseType.Sqlite) if (string.IsNullOrWhiteSpace(credential.Username) && !isFileBased)
errors.Add("Il nome utente è obbligatorio per questo tipo di database"); errors.Add("Il nome utente è obbligatorio per questo tipo di database");
if (string.IsNullOrWhiteSpace(credential.Password) && credential.DatabaseType != DatabaseType.Sqlite) if (string.IsNullOrWhiteSpace(credential.Password) && !isFileBased)
errors.Add("La password è obbligatoria per questo tipo di database"); errors.Add("La password è obbligatoria per questo tipo di database");
if (credential.Port <= 0 && credential.DatabaseType != DatabaseType.Sqlite) if (credential.Port <= 0 && !isFileBased)
{ {
// Assegna porta predefinita se non specificata // Assegna porta predefinita se non specificata
credential.Port = GetDefaultPort(credential.DatabaseType); credential.Port = GetDefaultPort(credential.DatabaseType);
+34 -1
View File
@@ -58,7 +58,8 @@ public enum DatabaseType
DB2, DB2,
SapHana, SapHana,
Odbc, Odbc,
OleDb OleDb,
Foxpro
} }
/// <summary> /// <summary>
@@ -200,6 +201,7 @@ public static class ConnectionStringBuilder
DatabaseType.SapHana => BuildSapHanaConnectionString(credential), DatabaseType.SapHana => BuildSapHanaConnectionString(credential),
DatabaseType.Odbc => BuildOdbcConnectionString(credential), DatabaseType.Odbc => BuildOdbcConnectionString(credential),
DatabaseType.OleDb => BuildOleDbConnectionString(credential), DatabaseType.OleDb => BuildOleDbConnectionString(credential),
DatabaseType.Foxpro => BuildFoxproConnectionString(credential),
_ => throw new NotSupportedException($"Database type {credential.DatabaseType} not supported") _ => throw new NotSupportedException($"Database type {credential.DatabaseType} not supported")
}; };
} private static string BuildSqlServerConnectionString(DatabaseCredential credential) } private static string BuildSqlServerConnectionString(DatabaseCredential credential)
@@ -474,6 +476,37 @@ public static class ConnectionStringBuilder
return string.Join(";", builder); return string.Join(";", builder);
} }
/// <summary>
/// Costruisce la "connection string" per una sorgente Visual FoxPro / dBase.
/// FoxPro non usa un vero connection string: serve solo il percorso (file .dbc o cartella di .dbf),
/// salvato in <see cref="DatabaseCredential.DatabaseName"/>. Le opzioni encoding/record cancellati
/// vengono aggiunte come chiavi CodePage/IncludeDeleted, interpretate da FoxProReader.
/// </summary>
private static string BuildFoxproConnectionString(DatabaseCredential credential)
{
// Una connection string esplicita (es. incollata dall'utente) ha la precedenza.
if (!string.IsNullOrEmpty(credential.ConnectionString))
return credential.ConnectionString;
var path = !string.IsNullOrEmpty(credential.DatabaseName)
? credential.DatabaseName
: credential.Host;
var builder = new List<string> { $"Data Source={path}" };
if (credential.AdditionalParameters != null)
{
if (credential.AdditionalParameters.TryGetValue("Encoding", out var enc) && !string.IsNullOrWhiteSpace(enc))
builder.Add($"CodePage={enc}");
if (credential.AdditionalParameters.TryGetValue("IncludeDeleted", out var del) &&
del.Equals("true", StringComparison.OrdinalIgnoreCase))
builder.Add("IncludeDeleted=true");
}
return string.Join(";", builder);
}
private static void AddAdditionalParameters(List<string> builder, Dictionary<string, string>? additionalParams) private static void AddAdditionalParameters(List<string> builder, Dictionary<string, string>? additionalParams)
{ {
if (additionalParams != null) if (additionalParams != null)
@@ -23,6 +23,7 @@ public static class CredentialExtensions
CredentialManager.Models.DatabaseType.SapHana => DataConnection.Enums.DatabaseType.SapHana, CredentialManager.Models.DatabaseType.SapHana => DataConnection.Enums.DatabaseType.SapHana,
CredentialManager.Models.DatabaseType.Odbc => DataConnection.Enums.DatabaseType.Odbc, CredentialManager.Models.DatabaseType.Odbc => DataConnection.Enums.DatabaseType.Odbc,
CredentialManager.Models.DatabaseType.OleDb => DataConnection.Enums.DatabaseType.OleDb, CredentialManager.Models.DatabaseType.OleDb => DataConnection.Enums.DatabaseType.OleDb,
CredentialManager.Models.DatabaseType.Foxpro => DataConnection.Enums.DatabaseType.Foxpro,
_ => throw new NotSupportedException($"Database type {credentialDbType} not supported") _ => throw new NotSupportedException($"Database type {credentialDbType} not supported")
}; };
} }
@@ -43,6 +44,7 @@ public static class CredentialExtensions
DataConnection.Enums.DatabaseType.SapHana => CredentialManager.Models.DatabaseType.SapHana, DataConnection.Enums.DatabaseType.SapHana => CredentialManager.Models.DatabaseType.SapHana,
DataConnection.Enums.DatabaseType.Odbc => CredentialManager.Models.DatabaseType.Odbc, DataConnection.Enums.DatabaseType.Odbc => CredentialManager.Models.DatabaseType.Odbc,
DataConnection.Enums.DatabaseType.OleDb => CredentialManager.Models.DatabaseType.OleDb, DataConnection.Enums.DatabaseType.OleDb => CredentialManager.Models.DatabaseType.OleDb,
DataConnection.Enums.DatabaseType.Foxpro => CredentialManager.Models.DatabaseType.Foxpro,
_ => throw new NotSupportedException($"Database type {dataConnectionDbType} not supported") _ => throw new NotSupportedException($"Database type {dataConnectionDbType} not supported")
}; };
} }
@@ -266,6 +266,7 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
CredentialManager.Models.DatabaseType.Sqlite => await TestSqliteConnection(connectionString, credential), CredentialManager.Models.DatabaseType.Sqlite => await TestSqliteConnection(connectionString, credential),
CredentialManager.Models.DatabaseType.Odbc => await TestOdbcConnection(connectionString, credential), CredentialManager.Models.DatabaseType.Odbc => await TestOdbcConnection(connectionString, credential),
CredentialManager.Models.DatabaseType.OleDb => await TestOleDbConnection(connectionString, credential), CredentialManager.Models.DatabaseType.OleDb => await TestOleDbConnection(connectionString, credential),
CredentialManager.Models.DatabaseType.Foxpro => await TestFoxproConnection(connectionString, credential),
_ => (false, $"Test di connessione non implementato per {credential.DatabaseType}") _ => (false, $"Test di connessione non implementato per {credential.DatabaseType}")
}; };
} }
@@ -459,6 +460,40 @@ public class DataConnectionCredentialService : IDataConnectionCredentialService
} }
} }
private async Task<(bool Success, string Message)> TestFoxproConnection(string connectionString, DatabaseCredential credential)
{
try
{
return await Task.Run(() =>
{
var info = DataConnection.DB.FoxPro.FoxProReader.Resolve(connectionString);
DataConnection.DB.FoxPro.FoxProReader.EnsureReachable(info);
var tables = DataConnection.DB.FoxPro.FoxProReader.GetTableNames(info);
var details = new System.Text.StringBuilder();
details.AppendLine("Connessione Visual FoxPro riuscita! (lettura managed, 64-bit)");
details.AppendLine();
details.AppendLine("Dettagli:");
details.AppendLine(info.IsContainer
? $"- Database container: {info.DbcPath}"
: $"- Cartella tabelle libere: {info.Folder}");
details.AppendLine($"- Tabelle rilevate: {tables.Count}");
details.AppendLine($"- Encoding: code page {info.Encoding.CodePage}");
details.AppendLine($"- Record cancellati: {(info.IncludeDeleted ? "inclusi" : "esclusi")}");
details.AppendLine("- Modalità: sola lettura (sorgente dati)");
return tables.Count > 0
? (true, details.ToString())
: (false, $"Percorso raggiungibile ma nessuna tabella .dbf trovata in: {info.Folder}");
});
}
catch (Exception ex)
{
return (false, $"Errore Visual FoxPro: {ex.Message}");
}
}
public async Task<(bool Success, string Message)> TestRestApiConnectionAsync(string credentialName) public async Task<(bool Success, string Message)> TestRestApiConnectionAsync(string credentialName)
{ {
try try
@@ -22,6 +22,7 @@ public class DatabaseSchemaProviderFactory
DatabaseType.SqlServer => new SqlServerSchemaProvider(), DatabaseType.SqlServer => new SqlServerSchemaProvider(),
DatabaseType.Odbc => new OdbcSchemaProvider(), DatabaseType.Odbc => new OdbcSchemaProvider(),
DatabaseType.OleDb => new OleDbSchemaProvider(), DatabaseType.OleDb => new OleDbSchemaProvider(),
DatabaseType.Foxpro => new FoxProSchemaProvider(),
// Aggiungere qui altri provider quando implementati // Aggiungere qui altri provider quando implementati
// DatabaseType.MySql => new MySqlSchemaProvider(), // DatabaseType.MySql => new MySqlSchemaProvider(),
// DatabaseType.PostgreSql => new PostgreSqlSchemaProvider(), // DatabaseType.PostgreSql => new PostgreSqlSchemaProvider(),
@@ -0,0 +1,54 @@
using DataConnection.DB.FoxPro;
using DataConnection.Interfaces;
namespace DataConnection.EF.SchemaProviders;
/// <summary>
/// Provider di schema per database Visual FoxPro / dBase, completamente managed
/// (legge i file <c>.dbf</c>/<c>.fpt</c>/<c>.dbc</c> senza provider OLE DB/ODBC).
/// La "connection string" è il percorso al <c>.dbc</c> o alla cartella di tabelle libere.
/// </summary>
public class FoxProSchemaProvider : IDatabaseSchemaProvider
{
public Task<IDictionary<string, IEnumerable<DbColumnInfo>>> GetDatabaseSchemaAsync(string connectionString)
{
var info = FoxProReader.Resolve(connectionString);
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)
{
// Una tabella corrotta/illeggibile non deve bloccare l'intera discovery.
Console.WriteLine($"[FoxPro] Impossibile leggere lo schema della tabella {table}: {ex.Message}");
}
}
return Task.FromResult<IDictionary<string, IEnumerable<DbColumnInfo>>>(result);
}
public Task<IEnumerable<string>> GetTableNamesAsync(string connectionString)
{
var info = FoxProReader.Resolve(connectionString);
return Task.FromResult<IEnumerable<string>>(FoxProReader.GetTableNames(info));
}
public Task<IEnumerable<DbColumnInfo>> GetTableSchemaAsync(string connectionString, string tableName)
{
var info = FoxProReader.Resolve(connectionString);
return Task.FromResult<IEnumerable<DbColumnInfo>>(FoxProReader.GetTableColumns(info, tableName));
}
public Task<IEnumerable<string>> GetAvailableDatabasesAsync(string connectionString)
{
// Sorgente file-based: un solo "database" (il container o la cartella).
var info = FoxProReader.Resolve(connectionString);
return Task.FromResult<IEnumerable<string>>(new[] { info.DisplayName });
}
}
+2 -1
View File
@@ -13,5 +13,6 @@ public enum DatabaseType
DB2, DB2,
SapHana, SapHana,
Odbc, Odbc,
OleDb OleDb,
Foxpro
} }
@@ -0,0 +1,35 @@
using System.Text;
namespace DataConnection.DB.FoxPro;
/// <summary>
/// Informazioni di connessione risolte per una sorgente Visual FoxPro / dBase.
/// Una "connessione" FoxPro è semplicemente un percorso sul filesystem:
/// un file <c>.dbc</c> (database container) oppure una cartella di tabelle libere <c>.dbf</c>.
/// </summary>
public sealed class FoxProConnectionInfo
{
/// <summary>Cartella che contiene i file <c>.dbf</c> (e relativi <c>.fpt</c>/<c>.cdx</c>).</summary>
public string Folder { get; init; } = string.Empty;
/// <summary>Percorso completo del file <c>.dbc</c>, se la sorgente è un database container; altrimenti null.</summary>
public string? DbcPath { get; init; }
/// <summary>
/// Encoding usato per decodificare i campi carattere/memo. Il valore reale è sempre
/// impostato da <see cref="FoxProReader.Resolve"/> (default effettivo: code page 1252);
/// qui usiamo Latin1 come placeholder built-in che non richiede il provider code-page.
/// </summary>
public Encoding Encoding { get; init; } = Encoding.Latin1;
/// <summary>Se true, include anche i record marcati come cancellati (default: false).</summary>
public bool IncludeDeleted { get; init; }
/// <summary>True se la sorgente è un database container (.dbc).</summary>
public bool IsContainer => !string.IsNullOrEmpty(DbcPath);
/// <summary>Nome leggibile della sorgente (nome del .dbc o della cartella), per messaggi/log.</summary>
public string DisplayName => IsContainer
? System.IO.Path.GetFileName(DbcPath!)
: new System.IO.DirectoryInfo(Folder).Name;
}
+422
View File
@@ -0,0 +1,422 @@
using System.Text;
using System.Text.RegularExpressions;
using DbfDataReader;
using DataConnection.Interfaces;
namespace DataConnection.DB.FoxPro;
/// <summary>
/// Helper di basso livello per leggere database Visual FoxPro / dBase in modo
/// completamente managed (nessun provider OLE DB/ODBC richiesto, funziona a 64-bit).
///
/// Legge direttamente i file <c>.dbf</c> (tabelle), <c>.fpt</c> (campi memo) e, quando
/// la sorgente è un container <c>.dbc</c>, ne legge il catalogo per ricavare l'elenco
/// ufficiale delle tabelle. La libreria sottostante è <c>DbfDataReader</c> (MIT).
///
/// Sola lettura: il coupler usa FoxPro come sorgente dati.
/// </summary>
public static class FoxProReader
{
static FoxProReader()
{
// Le code page non-Unicode (es. 1252, 1250, 850) non sono disponibili di default
// su .NET Core/5+: serve registrare il provider.
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
}
/// <summary>Code page di default (Windows-1252, ANSI Europa occidentale).</summary>
public const int DefaultCodePage = 1252;
// Mappa byte "language driver" dell'header DBF -> code page più comuni.
private static readonly Dictionary<byte, int> LanguageDriverCodePages = new()
{
[0x01] = 437, // U.S. MS-DOS
[0x02] = 850, // International MS-DOS
[0x03] = 1252, // Windows ANSI (più comune nei dati VFP)
[0x08] = 865, // Danish OEM
[0x09] = 437, // Dutch OEM
[0x0A] = 850, // Dutch OEM*
[0x64] = 852, // Eastern European MS-DOS
[0x65] = 866, // Russian MS-DOS
[0x66] = 865, // Nordic MS-DOS
[0x67] = 861, // Icelandic MS-DOS
[0x6A] = 737, // Greek MS-DOS
[0x6B] = 857, // Turkish MS-DOS
[0x57] = 1252, // ANSI
[0xC8] = 1250, // Eastern European Windows
[0xC9] = 1251, // Russian Windows
[0xCA] = 1254, // Turkish Windows
[0xCB] = 1253, // Greek Windows
};
/// <summary>
/// Interpreta la "connection string" FoxPro (un percorso, eventualmente con opzioni)
/// e risolve cartella, eventuale <c>.dbc</c>, encoding e gestione record cancellati.
///
/// Formati accettati:
/// <list type="bullet">
/// <item>percorso nudo a un file <c>.dbc</c>: <c>C:\dati\Data.dbc</c></item>
/// <item>percorso nudo a una cartella di <c>.dbf</c>: <c>C:\dati</c></item>
/// <item>stringa con chiavi: <c>Data Source=C:\dati\Data.dbc;CodePage=1252;IncludeDeleted=false</c></item>
/// <item>stringa stile OLE DB VFP: <c>Provider=vfpoledb;Data Source=C:\dati\Data.dbc;...</c> (Data Source viene estratto)</item>
/// </list>
/// </summary>
public static FoxProConnectionInfo Resolve(string connectionString)
{
if (string.IsNullOrWhiteSpace(connectionString))
throw new ArgumentException("Percorso del database FoxPro non specificato.", nameof(connectionString));
string? path = null;
int? codePage = null;
bool includeDeleted = false;
// Se contiene almeno una coppia chiave=valore, interpretiamo come connection string;
// altrimenti l'intera stringa è il percorso.
if (connectionString.Contains('='))
{
foreach (var segment in connectionString.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
var idx = segment.IndexOf('=');
if (idx < 0)
{
// Segmento senza '=': potrebbe essere un percorso nudo mescolato — usalo se non abbiamo già un path
path ??= segment.Trim();
continue;
}
var key = segment[..idx].Trim();
var value = segment[(idx + 1)..].Trim().Trim('"', '\'');
switch (key.Replace(" ", "").ToLowerInvariant())
{
case "datasource":
case "source":
case "data":
path = value;
break;
case "codepage":
case "encoding":
if (int.TryParse(value, out var cp))
codePage = cp;
else
codePage = TryGetCodePageByName(value);
break;
case "includedeleted":
case "deleted":
includeDeleted = value.Equals("true", StringComparison.OrdinalIgnoreCase)
|| value.Equals("on", StringComparison.OrdinalIgnoreCase)
|| value == "1";
break;
// "Provider", "Collating Sequence", ecc. sono ignorati (compatibilità OLE DB)
}
}
}
else
{
path = connectionString.Trim();
}
path = path?.Trim().Trim('"', '\'');
if (string.IsNullOrWhiteSpace(path))
throw new ArgumentException("Impossibile determinare il percorso del database FoxPro dalla stringa fornita.", nameof(connectionString));
string folder;
string? dbcPath = null;
if (path.EndsWith(".dbc", StringComparison.OrdinalIgnoreCase))
{
dbcPath = path;
folder = Path.GetDirectoryName(path) ?? path;
}
else
{
folder = path.TrimEnd('\\', '/');
// Se la cartella contiene un singolo .dbc e nessuna ambiguità, lo usiamo come container.
if (Directory.Exists(folder))
{
var dbcs = Directory.GetFiles(folder, "*.dbc");
if (dbcs.Length == 1)
dbcPath = dbcs[0];
}
}
var encoding = ResolveEncoding(codePage, dbcPath, folder);
return new FoxProConnectionInfo
{
Folder = folder,
DbcPath = dbcPath,
Encoding = encoding,
IncludeDeleted = includeDeleted
};
}
/// <summary>Verifica che la sorgente sia raggiungibile (cartella esistente, eventuale .dbc presente).</summary>
public static void EnsureReachable(FoxProConnectionInfo info)
{
if (info.IsContainer && !File.Exists(info.DbcPath!))
throw new FileNotFoundException($"File database container (.dbc) non trovato: {info.DbcPath}");
if (!Directory.Exists(info.Folder))
throw new DirectoryNotFoundException($"Cartella del database FoxPro non trovata: {info.Folder}");
}
/// <summary>
/// Elenco delle tabelle: dal catalogo del <c>.dbc</c> (OBJECTTYPE='Table') se container,
/// altrimenti enumerando i file <c>.dbf</c> nella cartella. Vengono restituite solo le
/// tabelle effettivamente apribili (file <c>.dbf</c> presente).
/// </summary>
public static List<string> GetTableNames(FoxProConnectionInfo info)
{
EnsureReachable(info);
var names = new List<string>();
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (info.IsContainer)
{
foreach (var name in ReadTableNamesFromContainer(info))
{
if (seen.Add(name) && File.Exists(ResolveTableFilePath(info.Folder, name)))
names.Add(name);
}
}
else
{
foreach (var file in Directory.EnumerateFiles(info.Folder, "*.dbf", SearchOption.TopDirectoryOnly))
{
var name = Path.GetFileNameWithoutExtension(file);
if (seen.Add(name))
names.Add(name);
}
}
names.Sort(StringComparer.OrdinalIgnoreCase);
return names;
}
/// <summary>Schema (colonne) di una singola tabella.</summary>
public static List<DbColumnInfo> GetTableColumns(FoxProConnectionInfo info, string tableName)
{
var filePath = ResolveTableFilePath(info.Folder, tableName);
if (!File.Exists(filePath))
throw new FileNotFoundException($"Tabella FoxPro non trovata: {filePath}");
using var table = new DbfTable(filePath, info.Encoding);
var columns = new List<DbColumnInfo>();
foreach (var col in table.Columns)
{
columns.Add(new DbColumnInfo
{
Name = col.ColumnName.Trim(),
DataType = MapColumnType(col),
IsNullable = true, // VFP: nullability non esposta in modo affidabile dall'header
IsPrimaryKey = false // le tabelle .dbf non hanno una PK affidabile; la chiave si sceglie nel coupler
});
}
return columns;
}
/// <summary>
/// Legge i record di una tabella come dizionari nome-colonna -> valore.
/// I valori null sono rappresentati con <see cref="DBNull.Value"/> (coerente con gli altri manager).
/// Streaming: non carica l'intera tabella in memoria.
/// </summary>
public static IEnumerable<Dictionary<string, object>> ReadRecords(
FoxProConnectionInfo info, string tableName, int? maxRows = null)
{
var filePath = ResolveTableFilePath(info.Folder, tableName);
if (!File.Exists(filePath))
throw new FileNotFoundException($"Tabella FoxPro non trovata: {filePath}");
var options = new DbfDataReaderOptions
{
Encoding = info.Encoding,
SkipDeletedRecords = !info.IncludeDeleted
};
using var reader = new DbfDataReader.DbfDataReader(filePath, options);
// Nomi colonna (trimmati) per ordinale.
var names = reader.DbfTable.Columns.Select(c => c.ColumnName.Trim()).ToArray();
long emitted = 0;
while (reader.Read())
{
var row = new Dictionary<string, object>(names.Length, StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < names.Length; i++)
{
row[names[i]] = reader.IsDBNull(i) ? DBNull.Value : reader.GetValue(i);
}
yield return row;
emitted++;
if (maxRows.HasValue && emitted >= maxRows.Value)
yield break;
}
}
/// <summary>Numero di record (header) di una tabella, senza scorrere i dati.</summary>
public static long GetRecordCount(FoxProConnectionInfo info, string tableName)
{
var filePath = ResolveTableFilePath(info.Folder, tableName);
if (!File.Exists(filePath))
return 0;
using var table = new DbfTable(filePath, info.Encoding);
return table.Header.RecordCount;
}
/// <summary>
/// Estrae il nome tabella (e un eventuale <c>TOP n</c>) da una semplice query SELECT.
/// Gestisce anche le query "wrappate" generate da CreateLimitedQuery
/// (<c>SELECT TOP n * FROM (SELECT ... FROM tabella) AS subquery</c>): viene preso
/// l'ultimo identificatore dopo FROM, cioè la tabella reale più interna.
/// </summary>
public static (string Table, int? Top)? ParseSimpleSelect(string sql)
{
if (string.IsNullOrWhiteSpace(sql))
return null;
var fromMatches = Regex.Matches(sql, @"\bFROM\s+\[?([A-Za-z_][\w]*)\]?", RegexOptions.IgnoreCase);
if (fromMatches.Count == 0)
return null;
var table = fromMatches[^1].Groups[1].Value;
int? top = null;
var topMatch = Regex.Match(sql, @"\bTOP\s+(\d+)", RegexOptions.IgnoreCase);
if (topMatch.Success && int.TryParse(topMatch.Groups[1].Value, out var t))
top = t;
return (table, top);
}
/// <summary>Risolve il path del file .dbf per una tabella, gestendo il case-insensitive di Windows.</summary>
public static string ResolveTableFilePath(string folder, string tableName)
{
var name = tableName.Trim().Trim('[', ']', '"', '\'');
if (name.EndsWith(".dbf", StringComparison.OrdinalIgnoreCase))
name = name[..^4];
var direct = Path.Combine(folder, name + ".dbf");
if (File.Exists(direct))
return direct;
// Ricerca case-insensitive (i cataloghi .dbc spesso usano un case diverso dai file).
if (Directory.Exists(folder))
{
foreach (var file in Directory.EnumerateFiles(folder, "*.dbf", SearchOption.TopDirectoryOnly))
{
if (string.Equals(Path.GetFileNameWithoutExtension(file), name, StringComparison.OrdinalIgnoreCase))
return file;
}
}
return direct; // non trovato: il chiamante gestirà il FileNotFound
}
private static IEnumerable<string> ReadTableNamesFromContainer(FoxProConnectionInfo info)
{
// Il file .dbc è esso stesso una tabella DBF: il suo catalogo contiene righe con
// OBJECTTYPE in {Database, Table, Index, Field, Relation, View}. Ci interessano i 'Table'.
var options = new DbfDataReaderOptions
{
Encoding = info.Encoding,
SkipDeletedRecords = true
};
using var reader = new DbfDataReader.DbfDataReader(info.DbcPath!, options);
int typeIdx = -1, nameIdx = -1;
for (int i = 0; i < reader.DbfTable.Columns.Count; i++)
{
var col = reader.DbfTable.Columns[i].ColumnName.Trim();
if (col.Equals("OBJECTTYPE", StringComparison.OrdinalIgnoreCase)) typeIdx = i;
else if (col.Equals("OBJECTNAME", StringComparison.OrdinalIgnoreCase)) nameIdx = i;
}
if (typeIdx < 0 || nameIdx < 0)
yield break; // .dbc non nel formato atteso
while (reader.Read())
{
var type = reader.IsDBNull(typeIdx) ? null : reader.GetValue(typeIdx)?.ToString()?.Trim();
if (!string.Equals(type, "Table", StringComparison.OrdinalIgnoreCase))
continue;
var name = reader.IsDBNull(nameIdx) ? null : reader.GetValue(nameIdx)?.ToString()?.Trim();
if (!string.IsNullOrEmpty(name))
yield return name;
}
}
private static string MapColumnType(DbfColumn col)
{
return col.ColumnType switch
{
DbfColumnType.Character => $"Character({col.Length})",
DbfColumnType.WideCharacter => $"Varchar({col.Length})",
DbfColumnType.Memo => "Memo",
DbfColumnType.Number => col.DecimalCount > 0
? $"Numeric({col.Length},{col.DecimalCount})"
: $"Numeric({col.Length})",
DbfColumnType.Float => $"Float({col.Length},{col.DecimalCount})",
DbfColumnType.Double => "Double",
DbfColumnType.SignedLong => "Integer",
DbfColumnType.Currency => "Currency",
DbfColumnType.Date => "Date",
DbfColumnType.DateTime => "DateTime",
DbfColumnType.Boolean => "Logical",
DbfColumnType.General => "General",
_ => col.ColumnType.ToString()
};
}
private static Encoding ResolveEncoding(int? overrideCodePage, string? dbcPath, string folder)
{
// 1) Override esplicito dell'utente
if (overrideCodePage.HasValue)
{
var enc = TryGetEncoding(overrideCodePage.Value);
if (enc != null) return enc;
}
// 2) Auto-detect dal byte "language driver" dell'header (dbc o prima tabella della cartella)
try
{
string? sampleDbf = dbcPath;
if (sampleDbf == null && Directory.Exists(folder))
sampleDbf = Directory.EnumerateFiles(folder, "*.dbf", SearchOption.TopDirectoryOnly).FirstOrDefault();
if (sampleDbf != null && File.Exists(sampleDbf))
{
using var fs = File.OpenRead(sampleDbf);
var header = new DbfHeader(fs);
if (LanguageDriverCodePages.TryGetValue(header.LanguageDriver, out var cp))
{
var enc = TryGetEncoding(cp);
if (enc != null) return enc;
}
}
}
catch
{
// ignora: si usa il default
}
// 3) Default
return Encoding.GetEncoding(DefaultCodePage);
}
private static Encoding? TryGetEncoding(int codePage)
{
try { return Encoding.GetEncoding(codePage); }
catch { return null; }
}
private static int? TryGetCodePageByName(string name)
{
try { return Encoding.GetEncoding(name).CodePage; }
catch { return null; }
}
}
+137
View File
@@ -0,0 +1,137 @@
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);
}
}
+3
View File
@@ -18,6 +18,9 @@
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" /> <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" />
<PackageReference Include="System.Data.Odbc" Version="9.0.3" /> <PackageReference Include="System.Data.Odbc" Version="9.0.3" />
<PackageReference Include="System.Data.OleDb" Version="9.0.3" /> <PackageReference Include="System.Data.OleDb" Version="9.0.3" />
<!-- Visual FoxPro / dBase: lettura managed di .dbf/.fpt/.dbc (64-bit, nessun provider OLE DB richiesto).
DbfDataReader porta transitivamente System.Text.Encoding.CodePages (per le code page non-Unicode). -->
<PackageReference Include="DbfDataReader" Version="1.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -90,6 +90,19 @@ public partial class DataCoupler : ComponentBase
var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential); var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential);
return credential?.DatabaseType == DatabaseType.OleDb; return credential?.DatabaseType == DatabaseType.OleDb;
} }
/// <summary>
/// Verifica se la credenziale database selezionata è di tipo Visual FoxPro
/// (sorgente file-based in sola lettura).
/// </summary>
protected bool IsFoxproConnection()
{
if (string.IsNullOrEmpty(selectedDatabaseCredential))
return false;
var credential = databaseCredentials.FirstOrDefault(c => c.Name == selectedDatabaseCredential);
return credential?.DatabaseType == DatabaseType.Foxpro;
}
/// <summary> /// <summary>
/// Gestisce il cambio di credenziale database selezionata /// Gestisce il cambio di credenziale database selezionata
@@ -760,6 +773,7 @@ public partial class DataCoupler : ComponentBase
{ {
DatabaseType.SqlServer => $"SELECT TOP {limit} * FROM ({baseQuery}) AS subquery", DatabaseType.SqlServer => $"SELECT TOP {limit} * FROM ({baseQuery}) AS subquery",
DatabaseType.OleDb => $"SELECT TOP {limit} * FROM ({baseQuery}) AS subquery", DatabaseType.OleDb => $"SELECT TOP {limit} * FROM ({baseQuery}) AS subquery",
DatabaseType.Foxpro => $"SELECT TOP {limit} * FROM ({baseQuery}) AS subquery",
DatabaseType.Oracle => $"SELECT * FROM ({baseQuery}) WHERE ROWNUM <= {limit}", DatabaseType.Oracle => $"SELECT * FROM ({baseQuery}) WHERE ROWNUM <= {limit}",
DatabaseType.MySql => $"{baseQuery} LIMIT {limit}", DatabaseType.MySql => $"{baseQuery} LIMIT {limit}",
DatabaseType.PostgreSql => $"{baseQuery} LIMIT {limit}", DatabaseType.PostgreSql => $"{baseQuery} LIMIT {limit}",
+152 -2
View File
@@ -244,6 +244,7 @@ else
<option value="@CredentialManager.Models.DatabaseType.SapHana">SAP HANA</option>*@ <option value="@CredentialManager.Models.DatabaseType.SapHana">SAP HANA</option>*@
<option value="@CredentialManager.Models.DatabaseType.Odbc">ODBC</option> <option value="@CredentialManager.Models.DatabaseType.Odbc">ODBC</option>
<option value="@CredentialManager.Models.DatabaseType.OleDb">OLE DB</option> <option value="@CredentialManager.Models.DatabaseType.OleDb">OLE DB</option>
<option value="@CredentialManager.Models.DatabaseType.Foxpro">Visual FoxPro (.dbc / .dbf)</option>
</InputSelect> </InputSelect>
</div> </div>
</div> </div>
@@ -609,6 +610,76 @@ else
</div> </div>
</div> </div>
} }
else if (currentDatabaseCredential.DatabaseType == CredentialManager.Models.DatabaseType.Foxpro)
{
<!-- Configurazione Visual FoxPro -->
<div class="card mb-3">
<div class="card-header bg-success text-white">
<h6 class="mb-0"><i class="oi oi-hard-drive"></i> Configurazione Visual FoxPro</h6>
</div>
<div class="card-body">
<div class="alert alert-info py-2">
<i class="oi oi-info"></i> Lettura <strong>managed</strong> dei file <code>.dbf</code>/<code>.fpt</code>/<code>.dbc</code>:
funziona a <strong>64-bit senza installare alcun provider</strong>. FoxPro è utilizzabile come
<strong>sorgente</strong> (sola lettura).
</div>
<div class="mb-3">
<label class="form-label">Percorso Database FoxPro *</label>
<InputText class="form-control font-monospace"
@bind-Value="currentDatabaseCredential.DatabaseName"
placeholder="es. C:\dati\Data.dbc oppure C:\dati" />
<small class="form-text text-muted">
<strong>Database container</strong>: percorso completo al file <code>.dbc</code>
(es. <code>C:\Users\aless\Desktop\data\Data.dbc</code>).<br />
<strong>Tabelle libere</strong>: percorso della <strong>cartella</strong> che contiene i file <code>.dbf</code>
(es. <code>C:\Users\aless\Desktop\data</code>).
</small>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Code Page (encoding)</label>
<select class="form-select" @bind="foxproCodePage">
<option value="">-- Auto / Default (1252) --</option>
<option value="1252">1252 — Europa occidentale (ANSI)</option>
<option value="1250">1250 — Europa centrale</option>
<option value="1251">1251 — Cirillico</option>
<option value="850">850 — DOS Latin-1</option>
<option value="437">437 — DOS US</option>
<option value="65001">65001 — UTF-8</option>
</select>
<small class="form-text text-muted">Per dati italiani lasciare 1252.</small>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label class="form-label">Record cancellati</label>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="foxproIncludeDeleted" @bind="foxproIncludeDeleted" />
<label class="form-check-label" for="foxproIncludeDeleted">
Includi i record marcati come cancellati
</label>
</div>
<small class="form-text text-muted">Di default i record cancellati vengono esclusi.</small>
</div>
</div>
</div>
<div class="alert alert-light border py-2 small mb-3">
<i class="oi oi-list"></i> Tipi VFP supportati: Character, Memo, Numeric, Float, Double,
Integer, Currency, Date, DateTime, Logical. I campi memo (<code>.fpt</code>) e gli indici
(<code>.cdx</code>) vengono rilevati automaticamente accanto al <code>.dbf</code>.
</div>
<div class="mb-3">
<label class="form-label">Anteprima percorso/connessione</label>
<textarea class="form-control font-monospace" rows="2" readonly>@GetFoxproConnectionStringPreview()</textarea>
</div>
</div>
</div>
}
else else
{ {
<!-- Configurazione Standard Database --> <!-- Configurazione Standard Database -->
@@ -1035,6 +1106,10 @@ else
private string oleDbCollatingSequence = string.Empty; private string oleDbCollatingSequence = string.Empty;
private string oleDbDeleted = string.Empty; private string oleDbDeleted = string.Empty;
// Visual FoxPro specific state
private string foxproCodePage = string.Empty;
private bool foxproIncludeDeleted = false;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ await RefreshCredentials(); { await RefreshCredentials();
CheckForProblematicCredentials(); CheckForProblematicCredentials();
@@ -1131,7 +1206,15 @@ else
if (currentDatabaseCredential.AdditionalParameters?.ContainsKey("DELETED") == true) if (currentDatabaseCredential.AdditionalParameters?.ContainsKey("DELETED") == true)
oleDbDeleted = currentDatabaseCredential.AdditionalParameters["DELETED"]; oleDbDeleted = currentDatabaseCredential.AdditionalParameters["DELETED"];
} }
// Se è Visual FoxPro, ripristina encoding e gestione record cancellati
if (currentDatabaseCredential.DatabaseType == DatabaseType.Foxpro)
{
foxproCodePage = currentDatabaseCredential.AdditionalParameters?.GetValueOrDefault("Encoding") ?? string.Empty;
var incDel = currentDatabaseCredential.AdditionalParameters?.GetValueOrDefault("IncludeDeleted", "") ?? "";
foxproIncludeDeleted = string.Equals(incDel, "true", StringComparison.OrdinalIgnoreCase);
}
showDatabaseModal = true; showDatabaseModal = true;
} }
@@ -1155,6 +1238,26 @@ else
currentDatabaseCredential.AdditionalParameters.Remove("DELETED"); currentDatabaseCredential.AdditionalParameters.Remove("DELETED");
} }
// Sincronizza i parametri Visual FoxPro negli AdditionalParameters
if (currentDatabaseCredential.DatabaseType == DatabaseType.Foxpro)
{
currentDatabaseCredential.AdditionalParameters ??= new Dictionary<string, string>();
if (!string.IsNullOrEmpty(foxproCodePage))
currentDatabaseCredential.AdditionalParameters["Encoding"] = foxproCodePage;
else
currentDatabaseCredential.AdditionalParameters.Remove("Encoding");
if (foxproIncludeDeleted)
currentDatabaseCredential.AdditionalParameters["IncludeDeleted"] = "true";
else
currentDatabaseCredential.AdditionalParameters.Remove("IncludeDeleted");
// Sorgente file-based: nessun host/porta/credenziali
currentDatabaseCredential.Host = string.Empty;
currentDatabaseCredential.Port = 0;
}
await CredentialService.SaveDatabaseCredentialAsync(currentDatabaseCredential); await CredentialService.SaveDatabaseCredentialAsync(currentDatabaseCredential);
await JSRuntime.InvokeVoidAsync("alert", "Credenziale database salvata con successo!"); await JSRuntime.InvokeVoidAsync("alert", "Credenziale database salvata con successo!");
CloseDatabaseModal(); CloseDatabaseModal();
@@ -1227,6 +1330,15 @@ else
} }
} }
} }
else if (currentDatabaseCredential.DatabaseType == DatabaseType.Foxpro)
{
// Visual FoxPro: sorgente file-based, richiede solo il percorso (DatabaseName)
if (string.IsNullOrWhiteSpace(currentDatabaseCredential.DatabaseName))
{
await JSRuntime.InvokeVoidAsync("alert", "Inserisci il percorso del database FoxPro (file .dbc o cartella con i .dbf).");
return;
}
}
else else
{ {
// Altri database: validazione standard (Host, Username, Password) // Altri database: validazione standard (Host, Username, Password)
@@ -1281,7 +1393,15 @@ else
{ {
await LoadOdbcData(); await LoadOdbcData();
} }
// Se è Visual FoxPro, reimposta i valori di default (sola lettura, file-based)
if (currentDatabaseCredential.DatabaseType == DatabaseType.Foxpro)
{
currentDatabaseCredential.AdditionalParameters ??= new Dictionary<string, string>();
foxproCodePage = string.Empty;
foxproIncludeDeleted = false;
}
StateHasChanged(); StateHasChanged();
} }
@@ -1531,6 +1651,36 @@ else
} }
} }
private string GetFoxproConnectionStringPreview()
{
if (currentDatabaseCredential.DatabaseType != DatabaseType.Foxpro)
return string.Empty;
try
{
currentDatabaseCredential.AdditionalParameters ??= new Dictionary<string, string>();
if (!string.IsNullOrEmpty(foxproCodePage))
currentDatabaseCredential.AdditionalParameters["Encoding"] = foxproCodePage;
else
currentDatabaseCredential.AdditionalParameters.Remove("Encoding");
if (foxproIncludeDeleted)
currentDatabaseCredential.AdditionalParameters["IncludeDeleted"] = "true";
else
currentDatabaseCredential.AdditionalParameters.Remove("IncludeDeleted");
if (string.IsNullOrWhiteSpace(currentDatabaseCredential.DatabaseName))
return "(inserire il percorso al file .dbc o alla cartella di .dbf)";
return ConnectionStringBuilder.BuildConnectionString(currentDatabaseCredential);
}
catch (Exception ex)
{
return $"Errore nella generazione: {ex.Message}";
}
}
#endregion #endregion
#endregion #endregion
@@ -91,6 +91,14 @@ namespace Data_Coupler.Services
return new DataConnection.DB.OleDbDatabaseManager(connectionString); 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 // Per altri database, usa EFCoreDatabaseManager
var dbManagerOptions = await _credentialService.GetDbManagerOptionsAsync(credential.Name); var dbManagerOptions = await _credentialService.GetDbManagerOptionsAsync(credential.Name);
return new EFCoreDatabaseManager(dbManagerOptions); return new EFCoreDatabaseManager(dbManagerOptions);
+106
View File
@@ -0,0 +1,106 @@
# Connessione a database Visual FoxPro
Data-Coupler supporta i database **Visual FoxPro / dBase** come **sorgente dati** (sola lettura)
tramite un lettore **completamente managed**: legge direttamente i file `.dbf` (tabelle),
`.fpt` (campi memo) e `.dbc` (database container) **senza installare alcun provider OLE DB/ODBC**
e funzionando a **64-bit**.
> ️ **Perché managed e non OLE DB?** Il provider `VFPOLEDB.1` di Microsoft è **solo a 32-bit**,
> deprecato e va installato a parte. Il lettore managed non ha questi vincoli e funziona ovunque
> giri l'applicazione (anche in container Linux/Windows a 64-bit).
---
## 1. Come connettersi (esempio)
La "connessione" FoxPro è semplicemente un **percorso**. Non serve host, utente o password.
### Passo passo nell'interfaccia
1. Vai su **Credenziali****Aggiungi credenziale database**.
2. **Nome**: un nome a piacere (es. `Gestionale FoxPro`).
3. **Tipo Database**: seleziona **Visual FoxPro (.dbc / .dbf)**.
4. **Percorso Database FoxPro**: inserisci uno dei due:
| Caso | Cosa inserire | Esempio |
|---|---|---|
| **Database container** | percorso completo al file `.dbc` | `C:\Users\aless\Desktop\data\Data.dbc` |
| **Tabelle libere** (free tables) | percorso della **cartella** con i `.dbf` | `C:\Users\aless\Desktop\data` |
5. (Opzionale) **Code Page**: lascia `1252` per dati italiani/europei occidentali.
6. (Opzionale) **Record cancellati**: lascia deselezionato per escludere i record marcati come cancellati.
7. Premi **Test connessione**: deve mostrare il numero di tabelle rilevate.
8. **Salva**.
A questo punto la credenziale è utilizzabile come **sorgente** nella pagina **Data Coupler**:
selezionala, attendi la discovery delle tabelle, scegli la tabella e procedi con il mapping.
---
## 2. Formato della connection string
Internamente la credenziale viene tradotta in una stringa interpretata dal lettore FoxPro.
Sono accettate tutte queste forme (utile se incolli un percorso o una stringa esistente):
```
C:\Users\aless\Desktop\data\Data.dbc
C:\Users\aless\Desktop\data
Data Source=C:\Users\aless\Desktop\data\Data.dbc;CodePage=1252;IncludeDeleted=false
Provider=vfpoledb;Data Source=C:\Users\aless\Desktop\data\Data.dbc;Collating Sequence=machine;
```
> Le stringhe stile **OLE DB** (`Provider=vfpoledb;Data Source=...`) sono accettate per comodità:
> viene estratto automaticamente il valore di `Data Source`. `Provider` e `Collating Sequence`
> vengono ignorati (il lettore è managed).
Opzioni riconosciute:
| Chiave | Significato | Default |
|---|---|---|
| `Data Source` | percorso al `.dbc` o alla cartella di `.dbf` | — (obbligatorio) |
| `CodePage` / `Encoding` | code page per i campi carattere/memo | auto-rilevata dall'header, fallback **1252** |
| `IncludeDeleted` | includere i record marcati come cancellati | `false` |
---
## 3. Tipi di dato supportati
| Tipo VFP | Mappato come |
|---|---|
| Character (`C`) | testo |
| Varchar (`V`) | testo |
| Memo (`M`, file `.fpt`) | testo |
| Numeric (`N`) | decimale |
| Float (`F`) | decimale |
| Double (`B`) | double |
| Integer (`I`) | intero |
| Currency (`Y`) | decimale |
| Date (`D`) | data |
| DateTime (`T`) | data/ora |
| Logical (`L`) | booleano |
I file memo (`.fpt`) e gli indici (`.cdx`) vengono individuati **automaticamente** accanto al `.dbf`.
---
## 4. Note e limitazioni
- **Sola lettura**: FoxPro è utilizzabile solo come **sorgente**. La scrittura sui `.dbf` non è
supportata (eviterebbe il rischio di corrompere indici `.cdx` e memo `.fpt` di gestionali legacy in uso).
- **Elenco tabelle**: con un `.dbc` viene letto il catalogo del container (solo le tabelle effettivamente
presenti come file `.dbf`); con una cartella vengono elencati tutti i `.dbf` presenti.
- **Query**: la sorgente lavora in modalità "tabella" (estrazione completa della tabella selezionata).
Le eventuali query custom supportano solo `SELECT [TOP n] * FROM <tabella>` (niente JOIN/WHERE evoluti).
- **Performance**: la lettura è in **streaming**; anche tabelle da centinaia di MB vengono elaborate
con un uso di memoria contenuto.
---
## 5. Risoluzione problemi
| Sintomo | Causa probabile | Soluzione |
|---|---|---|
| "Cartella del database FoxPro non trovata" | percorso errato | verifica il percorso del `.dbc` o della cartella |
| "nessuna tabella .dbf trovata" | cartella senza `.dbf` | indica la cartella giusta o il file `.dbc` |
| Caratteri accentati errati | code page diversa | imposta la **Code Page** corretta (es. 1250, 65001) |
| Una tabella non compare | `.dbf` referenziato nel `.dbc` ma file assente | ripristina il file `.dbf` mancante |