Files
Data-Coupler/CredentialManager/Models/CredentialModels.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

521 lines
20 KiB
C#

using System.ComponentModel.DataAnnotations;
namespace CredentialManager.Models;
/// <summary>
/// Tipi di credenziali supportate
/// </summary>
public enum CredentialType
{
Database,
RestApi,
OAuth,
ApiKey,
BasicAuth
}
/// <summary>
/// Tipi di servizi REST specifici
/// </summary>
public enum RestServiceType
{
Generic,
SapB1ServiceLayer,
Salesforce
}
/// <summary>
/// Tipo di flusso OAuth2 per Salesforce
/// </summary>
public enum SalesforceGrantType
{
/// <summary>
/// Flusso Username/Password (grant_type=password).
/// Richiede: ClientId, ClientSecret, Username, Password (+SecurityToken se non IP-trusted).
/// URL di login: https://login.salesforce.com o https://test.salesforce.com.
/// </summary>
Password,
/// <summary>
/// Flusso Client Credentials (grant_type=client_credentials) — server-to-server, senza utente.
/// Richiede: ClientId, ClientSecret.
/// URL obbligatorio: My Domain URL (es. https://myorg.my.salesforce.com).
/// La Connected App deve avere "Enable Client Credentials Flow" attivato e un Integration User assegnato.
/// </summary>
ClientCredentials
}
/// <summary>
/// Tipi di database supportati (allineato con DataConnection.Enums.DatabaseType)
/// </summary>
public enum DatabaseType
{
SqlServer,
MySql,
PostgreSql,
Oracle,
Sqlite,
DB2,
SapHana,
Odbc,
OleDb,
Foxpro
}
/// <summary>
/// Modalità di connessione ODBC
/// </summary>
public enum OdbcConnectionMode
{
/// <summary>
/// Utilizzo di un DSN (Data Source Name) configurato
/// </summary>
Dsn,
/// <summary>
/// Costruzione manuale della connection string
/// </summary>
Custom
}
/// <summary>
/// DTO per le credenziali database - completo per tutti i tipi di DB supportati
/// </summary>
public class DatabaseCredential
{
[Required(ErrorMessage = "Il nome è obbligatorio")]
public string Name { get; set; } = string.Empty;
public DatabaseType DatabaseType { get; set; }
public string Host { get; set; } = string.Empty;
public int Port { get; set; }
public string? DatabaseName { get; set; } = string.Empty; // Ora opzionale
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string? ConnectionString { get; set; }
public int CommandTimeout { get; set; } = 30;
public bool IgnoreSslErrors { get; set; } = false;
public Dictionary<string, string>? AdditionalParameters { get; set; }
// ODBC specific properties
public string? OdbcDsnName { get; set; } // Nome del DSN ODBC (se utilizzato)
public OdbcConnectionMode OdbcMode { get; set; } = OdbcConnectionMode.Dsn; // Modalità ODBC (DSN o Custom)
}
/// <summary>
/// DTO per le credenziali REST API - allineato con RestServiceOptions e esteso per servizi specifici
/// </summary>
public class RestApiCredential
{
[Required(ErrorMessage = "Il nome è obbligatorio")]
public string Name { get; set; } = string.Empty;
public RestServiceType ServiceType { get; set; } = RestServiceType.Generic;
public string BaseUrl { get; set; } = string.Empty;
public string? ApiKey { get; set; }
public string? Username { get; set; }
public string? Password { get; set; }
public string? AuthToken { get; set; }
public string? BearerToken { get; set; }
public int TimeoutSeconds { get; set; } = 100;
public bool IgnoreSslErrors { get; set; } = false;
public Dictionary<string, string>? Headers { get; set; }
public Dictionary<string, string>? AdditionalParameters { get; set; }
// Campi specifici per SAP B1 Service Layer
public string? CompanyDatabase { get; set; }
public string? Language { get; set; } = "en-US";
public string? Version { get; set; } = "v1";
public bool UseTrustedConnection { get; set; } = false;
// Campi specifici per Salesforce
public string? SecurityToken { get; set; }
public string? ClientId { get; set; }
public string? ClientSecret { get; set; }
public string? ApiVersion { get; set; } = "59.0";
public bool IsSandbox { get; set; } = false;
public bool UseSoapApi { get; set; } = false;
public SalesforceGrantType GrantType { get; set; } = SalesforceGrantType.Password;
public string? RefreshToken { get; set; }
public string? AccessToken { get; set; }
public DateTime? TokenExpiry { get; set; }
}
/// <summary>
/// Credenziali specifiche per SAP Business One Service Layer
/// </summary>
public class SapB1ServiceLayerCredential
{
public string Name { get; set; } = string.Empty;
public string ServerUrl { get; set; } = string.Empty; // es: https://server:50000/b1s/v1/
public string CompanyDatabase { get; set; } = string.Empty; // Database dell'azienda SAP
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string? Language { get; set; } = "en-US"; // Lingua per la sessione
public int TimeoutSeconds { get; set; } = 300; // Timeout per le chiamate API
public bool IgnoreSslErrors { get; set; } = false;
public string? Version { get; set; } = "v1"; // Versione del Service Layer (v1, v2, etc.)
public bool UseTrustedConnection { get; set; } = false; // Per autenticazione Windows
public Dictionary<string, string>? AdditionalHeaders { get; set; }
}
/// <summary>
/// Credenziali specifiche per Salesforce
/// </summary>
public class SalesforceCredential
{
public string Name { get; set; } = string.Empty;
public string LoginUrl { get; set; } = "https://login.salesforce.com"; // o https://test.salesforce.com per sandbox
public string Username { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public string SecurityToken { get; set; } = string.Empty; // Token di sicurezza Salesforce
public string? ClientId { get; set; } = string.Empty; // Consumer Key per Connected App
public string? ClientSecret { get; set; } = string.Empty; // Consumer Secret per Connected App
public string ApiVersion { get; set; } = "59.0"; // Versione API Salesforce
public bool IsSandbox { get; set; } = false; // Se è un ambiente sandbox
public int TimeoutSeconds { get; set; } = 120;
public bool UseSoapApi { get; set; } = false; // Se usare SOAP invece di REST
/// <summary>Tipo di flusso OAuth2 da utilizzare. Default: Password (retrocompatibile).</summary>
public SalesforceGrantType GrantType { get; set; } = SalesforceGrantType.Password;
public string? RefreshToken { get; set; }
public string? AccessToken { get; set; }
public DateTime? TokenExpiry { get; set; }
}
/// <summary>
/// Factory per creare connection string per i diversi tipi di database
/// </summary>
public static class ConnectionStringBuilder
{
public static string BuildConnectionString(DatabaseCredential credential)
{
if (!string.IsNullOrEmpty(credential.ConnectionString))
return credential.ConnectionString;
return credential.DatabaseType switch
{
DatabaseType.SqlServer => BuildSqlServerConnectionString(credential),
DatabaseType.MySql => BuildMySqlConnectionString(credential),
DatabaseType.PostgreSql => BuildPostgreSqlConnectionString(credential),
DatabaseType.Oracle => BuildOracleConnectionString(credential),
DatabaseType.Sqlite => BuildSqliteConnectionString(credential),
DatabaseType.DB2 => BuildDb2ConnectionString(credential),
DatabaseType.SapHana => BuildSapHanaConnectionString(credential),
DatabaseType.Odbc => BuildOdbcConnectionString(credential),
DatabaseType.OleDb => BuildOleDbConnectionString(credential),
DatabaseType.Foxpro => BuildFoxproConnectionString(credential),
_ => throw new NotSupportedException($"Database type {credential.DatabaseType} not supported")
};
} private static string BuildSqlServerConnectionString(DatabaseCredential credential)
{
var builder = new List<string>();
// Gestione speciale per SQL Server locale e named instances
// Se l'host contiene '\' (instance name) o '(localdb)', non aggiungere la porta
bool hasInstanceName = credential.Host.Contains('\\') ||
credential.Host.StartsWith("(localdb)", StringComparison.OrdinalIgnoreCase);
if (hasInstanceName)
{
// Per named instances e LocalDB, non includere la porta
builder.Add($"Server={credential.Host}");
}
else
{
// Per connessioni TCP/IP standard, include host e porta
// Ma solo se la porta non è la default (1433) per localhost
if ((credential.Host.Equals("localhost", StringComparison.OrdinalIgnoreCase) ||
credential.Host == "." ||
credential.Host == "127.0.0.1") && credential.Port == 1433)
{
// Per localhost con porta default, ometti la porta per usare Named Pipes
builder.Add($"Server={credential.Host}");
}
else
{
// Per altri casi, usa host,porta
builder.Add($"Server={credential.Host},{credential.Port}");
}
}
// Se username è vuoto o è "Integrated", usa Windows Authentication
if (string.IsNullOrWhiteSpace(credential.Username) ||
credential.Username.Equals("Integrated", StringComparison.OrdinalIgnoreCase) ||
credential.Username.Equals("Windows", StringComparison.OrdinalIgnoreCase))
{
builder.Add("Integrated Security=True");
}
else
{
// Usa SQL Server Authentication
builder.Add($"User Id={credential.Username}");
builder.Add($"Password={credential.Password}");
}
builder.Add($"Connection Timeout={credential.CommandTimeout}");
// Aggiungi Database solo se specificato
if (!string.IsNullOrEmpty(credential.DatabaseName))
builder.Add($"Database={credential.DatabaseName}");
if (credential.IgnoreSslErrors)
builder.Add("TrustServerCertificate=True");
AddAdditionalParameters(builder, credential.AdditionalParameters);
return string.Join(";", builder);
} private static string BuildMySqlConnectionString(DatabaseCredential credential)
{
var builder = new List<string>
{
$"Server={credential.Host}",
$"Port={credential.Port}",
$"Uid={credential.Username}",
$"Pwd={credential.Password}",
$"Connection Timeout={credential.CommandTimeout}"
};
// Aggiungi Database solo se specificato
if (!string.IsNullOrEmpty(credential.DatabaseName))
builder.Add($"Database={credential.DatabaseName}");
if (credential.IgnoreSslErrors)
builder.Add("SslMode=None");
AddAdditionalParameters(builder, credential.AdditionalParameters);
return string.Join(";", builder);
} private static string BuildPostgreSqlConnectionString(DatabaseCredential credential)
{
var builder = new List<string>
{
$"Host={credential.Host}",
$"Port={credential.Port}",
$"Username={credential.Username}",
$"Password={credential.Password}",
$"Timeout={credential.CommandTimeout}"
};
// Aggiungi Database solo se specificato
if (!string.IsNullOrEmpty(credential.DatabaseName))
builder.Add($"Database={credential.DatabaseName}");
if (credential.IgnoreSslErrors)
builder.Add("SSL Mode=Disable");
AddAdditionalParameters(builder, credential.AdditionalParameters);
return string.Join(";", builder);
} private static string BuildOracleConnectionString(DatabaseCredential credential)
{
var builder = new List<string>();
// Per Oracle, il formato cambia se c'è o meno un database specificato
if (!string.IsNullOrEmpty(credential.DatabaseName))
{
builder.Add($"Data Source={credential.Host}:{credential.Port}/{credential.DatabaseName}");
}
else
{
builder.Add($"Data Source={credential.Host}:{credential.Port}");
}
builder.AddRange(new[]
{
$"User Id={credential.Username}",
$"Password={credential.Password}",
$"Connection Timeout={credential.CommandTimeout}"
});
AddAdditionalParameters(builder, credential.AdditionalParameters);
return string.Join(";", builder);
}
private static string BuildSqliteConnectionString(DatabaseCredential credential)
{
var builder = new List<string>
{
$"Data Source={credential.DatabaseName}"
};
AddAdditionalParameters(builder, credential.AdditionalParameters);
return string.Join(";", builder);
}
private static string BuildDb2ConnectionString(DatabaseCredential credential)
{
var builder = new List<string>
{
$"Server={credential.Host}:{credential.Port}",
$"Database={credential.DatabaseName}",
$"UID={credential.Username}",
$"PWD={credential.Password}",
$"Connection Timeout={credential.CommandTimeout}"
};
AddAdditionalParameters(builder, credential.AdditionalParameters);
return string.Join(";", builder);
}
private static string BuildSapHanaConnectionString(DatabaseCredential credential)
{
var builder = new List<string>
{
$"Server={credential.Host}:{credential.Port}",
$"DatabaseName={credential.DatabaseName}",
$"UserID={credential.Username}",
$"Password={credential.Password}",
$"Connection Timeout={credential.CommandTimeout}"
};
AddAdditionalParameters(builder, credential.AdditionalParameters);
return string.Join(";", builder);
}
private static string BuildOdbcConnectionString(DatabaseCredential credential)
{
// Se è già presente una connection string personalizzata, utilizzala
if (!string.IsNullOrEmpty(credential.ConnectionString))
return credential.ConnectionString;
var builder = new List<string>();
// Modalità DSN: usa il DSN configurato
if (credential.OdbcMode == OdbcConnectionMode.Dsn && !string.IsNullOrEmpty(credential.OdbcDsnName))
{
builder.Add($"DSN={credential.OdbcDsnName}");
// Aggiungi credenziali se fornite
if (!string.IsNullOrEmpty(credential.Username))
builder.Add($"UID={credential.Username}");
if (!string.IsNullOrEmpty(credential.Password))
builder.Add($"PWD={credential.Password}");
}
// Modalità Custom: costruisci manualmente la connection string
else
{
// Driver (se specificato nei parametri aggiuntivi)
if (credential.AdditionalParameters?.ContainsKey("Driver") == true)
{
builder.Add($"Driver={{{credential.AdditionalParameters["Driver"]}}}");
}
// Server/Host
if (!string.IsNullOrEmpty(credential.Host))
{
builder.Add($"Server={credential.Host}");
// Porta (se diversa da 0)
if (credential.Port > 0)
builder.Add($"Port={credential.Port}");
}
// Database
if (!string.IsNullOrEmpty(credential.DatabaseName))
builder.Add($"Database={credential.DatabaseName}");
// Credenziali
if (!string.IsNullOrEmpty(credential.Username))
builder.Add($"UID={credential.Username}");
if (!string.IsNullOrEmpty(credential.Password))
builder.Add($"PWD={credential.Password}");
}
// Timeout
if (credential.CommandTimeout > 0)
builder.Add($"Connection Timeout={credential.CommandTimeout}");
// Parametri aggiuntivi (escludendo Driver se già aggiunto)
if (credential.AdditionalParameters != null)
{
foreach (var param in credential.AdditionalParameters)
{
if (param.Key != "Driver") // Driver già gestito sopra
builder.Add($"{param.Key}={param.Value}");
}
}
return string.Join(";", builder);
}
private static string BuildOleDbConnectionString(DatabaseCredential credential)
{
// Se è già presente una connection string personalizzata, utilizzala
if (!string.IsNullOrEmpty(credential.ConnectionString))
return credential.ConnectionString;
var builder = new List<string>();
// Provider OLE DB (obbligatorio)
var provider = credential.AdditionalParameters?.GetValueOrDefault("Provider") ?? "VFPOLEDB.1";
builder.Add($"Provider={provider}");
// Data Source: per VFP e Access è il percorso file/cartella
// DatabaseName è il campo principale (come per SQLite)
var dataSource = !string.IsNullOrEmpty(credential.DatabaseName)
? credential.DatabaseName
: credential.Host;
if (!string.IsNullOrEmpty(dataSource))
builder.Add($"Data Source={dataSource}");
// Credenziali (opzionali per VFP file-based)
if (!string.IsNullOrEmpty(credential.Username))
builder.Add($"User ID={credential.Username}");
if (!string.IsNullOrEmpty(credential.Password))
builder.Add($"Password={credential.Password}");
// Parametri aggiuntivi specifici (es. Collating Sequence, Exclusive, DELETED per VFP)
if (credential.AdditionalParameters != null)
{
foreach (var param in credential.AdditionalParameters)
{
if (param.Key != "Provider") // Provider già gestito sopra
builder.Add($"{param.Key}={param.Value}");
}
}
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)
{
if (additionalParams != null)
{
foreach (var param in additionalParams)
{
builder.Add($"{param.Key}={param.Value}");
}
}
}
}