From d1103c4e7d4e9bb368cd6ea53d965e4bd95dd483 Mon Sep 17 00:00:00 2001 From: Alessio Dal Santo Date: Mon, 28 Apr 2025 23:55:56 +0200 Subject: [PATCH] Implementato il supporto per la scoperta e la visualizzazione dello schema del database, inclusa la creazione di provider specifici per SQL Server e l'integrazione con il servizio di connessione al database. --- .../EF/DatabaseSchemaProviderFactory.cs | 33 +++ DataConnection/EF/DbManagerOptions.cs | 7 + DataConnection/EF/EFCoreDatabaseManager.cs | 52 +--- DataConnection/EF/ExistingDatabaseExample.cs | 0 .../SqlServerSchemaProvider.cs | 152 ++++++++++ .../Interfaces/IDatabaseSchemaProvider.cs | 17 ++ .../Data/DatabaseConnectionService.cs | 44 ++- Data_Coupler/Pages/DatabaseConnection.razor | 49 +--- Data_Coupler/Pages/DatabaseSchema.razor | 266 ++++++++++++++++++ Data_Coupler/Shared/NavMenu.razor | 5 + 10 files changed, 546 insertions(+), 79 deletions(-) create mode 100644 DataConnection/EF/DatabaseSchemaProviderFactory.cs create mode 100644 DataConnection/EF/ExistingDatabaseExample.cs create mode 100644 DataConnection/EF/SchemaProviders/SqlServerSchemaProvider.cs create mode 100644 DataConnection/Interfaces/IDatabaseSchemaProvider.cs create mode 100644 Data_Coupler/Pages/DatabaseSchema.razor diff --git a/DataConnection/EF/DatabaseSchemaProviderFactory.cs b/DataConnection/EF/DatabaseSchemaProviderFactory.cs new file mode 100644 index 0000000..25c54a6 --- /dev/null +++ b/DataConnection/EF/DatabaseSchemaProviderFactory.cs @@ -0,0 +1,33 @@ +using System; +using DataConnection.EF.SchemaProviders; +using DataConnection.Enums; +using DataConnection.Interfaces; + +namespace DataConnection.EF; + +/// +/// Factory per la creazione di provider di schema del database +/// +public class DatabaseSchemaProviderFactory +{ + /// + /// Crea un provider di schema in base al tipo di database + /// + /// Tipo di database + /// Provider di schema appropriato + public static IDatabaseSchemaProvider CreateProvider(DatabaseType databaseType) + { + return databaseType switch + { + DatabaseType.SqlServer => new SqlServerSchemaProvider(), + // Aggiungere qui altri provider quando implementati + // DatabaseType.MySql => new MySqlSchemaProvider(), + // DatabaseType.PostgreSql => new PostgreSqlSchemaProvider(), + // DatabaseType.Oracle => new OracleSchemaProvider(), + // DatabaseType.Sqlite => new SqliteSchemaProvider(), + // DatabaseType.DB2 => new DB2SchemaProvider(), + // DatabaseType.SapHana => new SapHanaSchemaProvider(), + _ => throw new NotSupportedException($"Tipo di database non supportato per l'estrazione dello schema: {databaseType}") + }; + } +} \ No newline at end of file diff --git a/DataConnection/EF/DbManagerOptions.cs b/DataConnection/EF/DbManagerOptions.cs index b3f61c8..81c8f5b 100644 --- a/DataConnection/EF/DbManagerOptions.cs +++ b/DataConnection/EF/DbManagerOptions.cs @@ -61,12 +61,19 @@ public class DbManagerOptions /// public string DatabaseName { get; set; } + /// + /// Tipo di database (SqlServer, MySql, ecc.) + /// + public DatabaseType DatabaseType { get; set; } = DatabaseType.SqlServer; + /// /// Configura automaticamente il servizio di scoperta database in base al tipo di database /// /// Tipo di database public void ConfigureDatabaseDiscovery(DatabaseType databaseType) { + DatabaseType = databaseType; + switch (databaseType) { case DatabaseType.SqlServer: diff --git a/DataConnection/EF/EFCoreDatabaseManager.cs b/DataConnection/EF/EFCoreDatabaseManager.cs index 54e06f6..4fcb5f8 100644 --- a/DataConnection/EF/EFCoreDatabaseManager.cs +++ b/DataConnection/EF/EFCoreDatabaseManager.cs @@ -106,50 +106,22 @@ public class EFCoreDatabaseManager : IDatabaseManager public async Task>> GetDatabaseSchemaAsync() { - // Assicurarsi che il contesto sia connesso - await _context.Database.OpenConnectionAsync(); - - var result = new Dictionary>(); - - // Ottiene tutte le entità dal modello - var model = _context.Model; - var entityTypes = model.GetEntityTypes().ToList(); - - foreach (var entityType in entityTypes) + try { - var tableName = entityType.GetTableName(); - var schemaName = entityType.GetSchema(); - var fullTableName = string.IsNullOrEmpty(schemaName) ? tableName : $"{schemaName}.{tableName}"; + // Assicurarsi che il contesto sia connesso + await _context.Database.OpenConnectionAsync(); - var columns = new List(); + // Usa la factory per ottenere il provider appropriato in base al tipo di database + var schemaProvider = DatabaseSchemaProviderFactory.CreateProvider(_options.DatabaseType); - foreach (var property in entityType.GetProperties()) - { - var columnInfo = new DbColumnInfo - { - Name = property.GetColumnName(), - DataType = property.GetColumnType(), - IsNullable = property.IsNullable, - IsPrimaryKey = property.IsPrimaryKey() - }; - - // Verifica se è una foreign key - var foreignKeys = entityType.GetForeignKeys().Where(fk => fk.Properties.Contains(property)); - if (foreignKeys.Any()) - { - var fk = foreignKeys.First(); - columnInfo.IsForeignKey = true; - columnInfo.ReferencedTable = fk.PrincipalEntityType.GetTableName(); - columnInfo.ReferencedColumn = fk.PrincipalKey.Properties.First().GetColumnName(); - } - - columns.Add(columnInfo); - } - - result.Add(fullTableName, columns); + // Usa il provider per ottenere lo schema + return await schemaProvider.GetDatabaseSchemaAsync(_context.Database.GetConnectionString()); + } + catch (Exception ex) + { + Console.WriteLine($"Errore nel recupero dello schema del database: {ex.Message}"); + throw; } - - return result; } public void Dispose() diff --git a/DataConnection/EF/ExistingDatabaseExample.cs b/DataConnection/EF/ExistingDatabaseExample.cs new file mode 100644 index 0000000..e69de29 diff --git a/DataConnection/EF/SchemaProviders/SqlServerSchemaProvider.cs b/DataConnection/EF/SchemaProviders/SqlServerSchemaProvider.cs new file mode 100644 index 0000000..5672365 --- /dev/null +++ b/DataConnection/EF/SchemaProviders/SqlServerSchemaProvider.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading.Tasks; +using DataConnection.Interfaces; +using Microsoft.Data.SqlClient; + +namespace DataConnection.EF.SchemaProviders; + +/// +/// Provider di schema per database SQL Server +/// +public class SqlServerSchemaProvider : IDatabaseSchemaProvider +{ + public async Task>> GetDatabaseSchemaAsync(string connectionString) + { + var result = new Dictionary>(); + + try + { + using (var connection = new SqlConnection(connectionString)) + { + await connection.OpenAsync(); + + // Query per ottenere la struttura delle tabelle in SQL Server + string sql = @" + SELECT + SCHEMA_NAME(t.schema_id) + '.' + t.name AS TableName, + c.name AS ColumnName, + tp.name AS DataType, + c.max_length, + c.precision, + c.scale, + c.is_nullable, + CASE WHEN pk.column_id IS NOT NULL THEN 1 ELSE 0 END AS IsPrimaryKey, + CASE WHEN fk.parent_column_id IS NOT NULL THEN 1 ELSE 0 END AS IsForeignKey, + SCHEMA_NAME(ref_t.schema_id) + '.' + ref_t.name AS ReferencedTable, + ref_c.name AS ReferencedColumn + FROM + sys.tables t + INNER JOIN + sys.columns c ON t.object_id = c.object_id + INNER JOIN + sys.types tp ON c.user_type_id = tp.user_type_id + LEFT JOIN + (SELECT + ic.object_id, + ic.column_id + FROM + sys.indexes i + INNER JOIN + sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id + WHERE + i.is_primary_key = 1) pk ON t.object_id = pk.object_id AND c.column_id = pk.column_id + LEFT JOIN + sys.foreign_key_columns fk ON t.object_id = fk.parent_object_id AND c.column_id = fk.parent_column_id + LEFT JOIN + sys.tables ref_t ON fk.referenced_object_id = ref_t.object_id + LEFT JOIN + sys.columns ref_c ON fk.referenced_object_id = ref_c.object_id AND fk.referenced_column_id = ref_c.column_id + WHERE + t.is_ms_shipped = 0 + ORDER BY + TableName, c.column_id"; + + using (var command = new SqlCommand(sql, connection)) + { + command.CommandType = CommandType.Text; + + using (var reader = await command.ExecuteReaderAsync()) + { + string currentTable = null; + List columns = null; + + while (await reader.ReadAsync()) + { + string tableName = reader.GetString(0); + + // Se stiamo passando a una nuova tabella, aggiungiamo la tabella precedente e creiamo una nuova lista + if (currentTable != tableName) + { + if (currentTable != null && columns != null && columns.Count > 0) + { + result[currentTable] = columns; + } + + currentTable = tableName; + columns = new List(); + } + + // Formato del tipo di dati con precisione e scala per tipi numerici o lunghezza per tipi stringa + string dataType = reader.GetString(2); + int maxLength = reader.GetInt16(3); + byte precision = reader.GetByte(4); + byte scale = reader.GetByte(5); + + // Formattazione tipo di dati per SQL Server + string formattedDataType = FormatSqlServerDataType(dataType, maxLength, precision, scale); + + var columnInfo = new DbColumnInfo + { + Name = reader.GetString(1), + DataType = formattedDataType, + IsNullable = reader.GetBoolean(6), + IsPrimaryKey = reader.GetInt32(7) == 1, + IsForeignKey = reader.GetInt32(8) == 1, + ReferencedTable = reader.IsDBNull(9) ? null : reader.GetString(9), + ReferencedColumn = reader.IsDBNull(10) ? null : reader.GetString(10) + }; + + columns?.Add(columnInfo); + } + + // Aggiungiamo l'ultima tabella + if (currentTable != null && columns != null && columns.Count > 0) + { + result[currentTable] = columns; + } + } + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Errore nel recupero dello schema del database SQL Server: {ex.Message}"); + throw; + } + + return result; + } + + private static string FormatSqlServerDataType(string dataType, int maxLength, byte precision, byte scale) + { + string formattedDataType = dataType; + + if (dataType == "nvarchar" || dataType == "varchar" || dataType == "char" || dataType == "nchar") + { + if (maxLength == -1) + formattedDataType += "(MAX)"; + else if (dataType.StartsWith("n")) // tipi Unicode - la lunghezza è in byte, dobbiamo dividerla per 2 + formattedDataType += $"({maxLength / 2})"; + else + formattedDataType += $"({maxLength})"; + } + else if (dataType == "decimal" || dataType == "numeric") + { + formattedDataType += $"({precision},{scale})"; + } + + return formattedDataType; + } +} \ No newline at end of file diff --git a/DataConnection/Interfaces/IDatabaseSchemaProvider.cs b/DataConnection/Interfaces/IDatabaseSchemaProvider.cs new file mode 100644 index 0000000..6ef0826 --- /dev/null +++ b/DataConnection/Interfaces/IDatabaseSchemaProvider.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace DataConnection.Interfaces; + +/// +/// Interfaccia per provider di estrazione dello schema del database +/// +public interface IDatabaseSchemaProvider +{ + /// + /// Estrae lo schema del database (tabelle e colonne) + /// + /// Stringa di connessione al database + /// Struttura gerarchica delle tabelle e delle loro colonne + Task>> GetDatabaseSchemaAsync(string connectionString); +} \ No newline at end of file diff --git a/Data_Coupler/Data/DatabaseConnectionService.cs b/Data_Coupler/Data/DatabaseConnectionService.cs index 177b352..4552fd4 100644 --- a/Data_Coupler/Data/DatabaseConnectionService.cs +++ b/Data_Coupler/Data/DatabaseConnectionService.cs @@ -6,6 +6,7 @@ using DataConnection.Interfaces; using DataConnection.EF; using DataConnection.EF.DatabaseDiscovery; using DataConnection.Enums; +using Microsoft.EntityFrameworkCore; namespace Data_Coupler.Data { @@ -13,6 +14,8 @@ namespace Data_Coupler.Data { private readonly IDatabaseDiscovery _sqlServerDiscovery; private readonly IDatabaseDiscovery _mySqlDiscovery; + private readonly DbManagerOptions _dbManagerOptions; + private IDatabaseManager _databaseManager; public DatabaseType SelectedDatabaseType { get; set; } = DatabaseType.SqlServer; public string ConnectionString { get; set; } @@ -22,9 +25,10 @@ namespace Data_Coupler.Data public bool IsConnected { get; private set; } public string ErrorMessage { get; private set; } - public DatabaseConnectionService() + public DatabaseConnectionService(DbManagerOptions dbManagerOptions) { _sqlServerDiscovery = new SqlServerDatabaseDiscovery(); + _dbManagerOptions = dbManagerOptions ?? new DbManagerOptions(); // Se in futuro verrà implementata la discovery MySQL, potremmo aggiungerla qui // _mySqlDiscovery = new MySqlDatabaseDiscovery(); } @@ -104,5 +108,43 @@ namespace Data_Coupler.Data // Per altri tipi di database, implementare la logica appropriata return ConnectionString; } + + public IDatabaseManager GetDatabaseManager() + { + if (_databaseManager != null) + return _databaseManager; + + // Configura le opzioni del database manager + _dbManagerOptions.ServerConnectionString = ConnectionString; + _dbManagerOptions.DatabaseName = SelectedDatabase; + + // Configura il contesto in base al tipo di database + _dbManagerOptions.DbContextConfigurator = options => + { + switch (SelectedDatabaseType) + { + case DatabaseType.SqlServer: + options.UseSqlServer(BuildConnectionStringWithDatabase(), + sqlOptions => sqlOptions.CommandTimeout(_dbManagerOptions.CommandTimeout)); + break; + // Aggiungi altri tipi di database quando implementati + default: + throw new NotSupportedException($"Tipo di database non supportato: {SelectedDatabaseType}"); + } + }; + + // Crea il database manager + _databaseManager = new EFCoreDatabaseManager(_dbManagerOptions); + return _databaseManager; + } + + public void DisposeDatabaseManager() + { + if (_databaseManager != null) + { + _databaseManager.Dispose(); + _databaseManager = null; + } + } } } \ No newline at end of file diff --git a/Data_Coupler/Pages/DatabaseConnection.razor b/Data_Coupler/Pages/DatabaseConnection.razor index dbc3ca7..d87ac59 100644 --- a/Data_Coupler/Pages/DatabaseConnection.razor +++ b/Data_Coupler/Pages/DatabaseConnection.razor @@ -241,17 +241,19 @@ databaseInfo = dbInfo; } - // Qui dovresti implementare la connessione al database selezionato - // e recuperare le informazioni sullo schema - // Per questo esempio, simuliamo che sia avvenuto con successo - await Task.Delay(1000); // Simulazione operazione asincrona + // Crea il database manager e connettiti al database selezionato + var databaseManager = DatabaseService.GetDatabaseManager(); + + // Verifica la connessione al database specifico + if (!await databaseManager.TestConnectionAsync()) + { + throw new Exception($"Non è possibile connettersi al database {DatabaseService.SelectedDatabase}"); + } + + // Recupera le informazioni sullo schema (tabelle e colonne) + schemaInfo = await databaseManager.GetDatabaseSchemaAsync(); showSchemaInfo = true; - // In un'implementazione reale, qui dovresti eseguire: - // schemaInfo = await databaseManager.GetDatabaseSchemaAsync(); - - // Per ora creiamo dati di esempio - schemaInfo = CreateSampleSchemaInfo(); } catch (Exception ex) { @@ -264,33 +266,4 @@ isConnecting = false; } } - - // Metodo temporaneo per generare dati di esempio - da sostituire con dati reali - private IDictionary> CreateSampleSchemaInfo() - { - var result = new Dictionary>(); - - // Tabella Customers - var customerColumns = new List - { - new DbColumnInfo { Name = "Id", DataType = "int", IsPrimaryKey = true }, - new DbColumnInfo { Name = "Name", DataType = "nvarchar(100)" }, - new DbColumnInfo { Name = "Email", DataType = "nvarchar(255)" }, - new DbColumnInfo { Name = "Created", DataType = "datetime" } - }; - - // Tabella Orders - var orderColumns = new List - { - new DbColumnInfo { Name = "Id", DataType = "int", IsPrimaryKey = true }, - new DbColumnInfo { Name = "CustomerId", DataType = "int", IsForeignKey = true, ReferencedTable = "Customers", ReferencedColumn = "Id" }, - new DbColumnInfo { Name = "OrderDate", DataType = "datetime" }, - new DbColumnInfo { Name = "TotalAmount", DataType = "decimal(18,2)" } - }; - - result.Add("Customers", customerColumns); - result.Add("Orders", orderColumns); - - return result; - } } \ No newline at end of file diff --git a/Data_Coupler/Pages/DatabaseSchema.razor b/Data_Coupler/Pages/DatabaseSchema.razor new file mode 100644 index 0000000..8359cfd --- /dev/null +++ b/Data_Coupler/Pages/DatabaseSchema.razor @@ -0,0 +1,266 @@ +@page "/database-schema" +@using DataConnection.Interfaces +@using DataConnection.Enums +@inject Data.DatabaseConnectionService DatabaseService +@inject IJSRuntime JSRuntime +@inject NavigationManager NavigationManager + +Schema del Database + +

Schema del Database

+ +@if (!DatabaseService.IsConnected || string.IsNullOrEmpty(DatabaseService.SelectedDatabase)) +{ +
+

Non sei connesso a nessun database. Devi prima stabilire una connessione.

+ +
+} +else +{ +
+
+
+
+ Dettagli del Database: @DatabaseService.SelectedDatabase + +
+
+ @if (databaseInfo != null) + { +
+
+ Nome: @databaseInfo.Name +
+
+ Dimensione: @databaseInfo.SizeMB.ToString("N2") MB +
+
+ Creato il: @databaseInfo.CreationDate.ToString("dd/MM/yyyy HH:mm") +
+
+ Stato: @databaseInfo.Status +
+
+ } +
+
+
+
+ +
+
+
+
+ Tabelle +
+
+
+ + + @if (schemaInfo != null && schemaInfo.Count > 0) + { + @foreach (var table in filteredTables) + { + + } + } + else if (isLoading) + { +
+
+ Caricamento... +
+
+ } + else + { +
+ Nessuna tabella disponibile +
+ } +
+
+
+
+ +
+
+
+ @if (!string.IsNullOrEmpty(selectedTable)) + { + Struttura della tabella: @selectedTable + } + else + { + Seleziona una tabella + } +
+
+ @if (!string.IsNullOrEmpty(selectedTable) && schemaInfo != null && schemaInfo.ContainsKey(selectedTable)) + { +
+ + + + + + + + + + + + @foreach (var column in schemaInfo[selectedTable]) + { + + + + + + + + } + +
Nome ColonnaTipo DatiNullableChiaveRelazioni
@column.Name@column.DataType + @if (column.IsNullable) + { + NULL + } + else + { + NOT NULL + } + + @if (column.IsPrimaryKey) + { + PK + } + + @if (column.IsForeignKey) + { + + FK → @column.ReferencedTable.@column.ReferencedColumn + + } +
+
+ } + else if (string.IsNullOrEmpty(selectedTable)) + { +
+ Seleziona una tabella dal menu a sinistra per visualizzarne la struttura. +
+ } + else if (isLoading) + { +
+
+ Caricamento... +
+
+ } + else + { +
+ Impossibile trovare informazioni sulla tabella selezionata. +
+ } +
+
+
+
+} + +@code { + private bool isLoading = false; + private string selectedTable = ""; + private string tableFilter = ""; + private DatabaseInfo databaseInfo; + private IDictionary> schemaInfo; + private IDictionary> filteredTables; + + protected override async Task OnInitializedAsync() + { + if (DatabaseService.IsConnected && !string.IsNullOrEmpty(DatabaseService.SelectedDatabase)) + { + await LoadDatabaseSchema(); + } + } + + private async Task LoadDatabaseSchema() + { + try + { + isLoading = true; + selectedTable = ""; + + // Ottieni informazioni sul database selezionato + if (DatabaseService.DatabasesInfo.TryGetValue(DatabaseService.SelectedDatabase, out var dbInfo)) + { + databaseInfo = dbInfo; + } + + // Crea il database manager e connettiti al database selezionato + var databaseManager = DatabaseService.GetDatabaseManager(); + + // Verifica la connessione al database specifico + if (await databaseManager.TestConnectionAsync()) + { + // Recupera le informazioni sullo schema (tabelle e colonne) + schemaInfo = await databaseManager.GetDatabaseSchemaAsync(); + filteredTables = schemaInfo; + } + else + { + throw new Exception($"Non è possibile connettersi al database {DatabaseService.SelectedDatabase}"); + } + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("console.error", "Errore caricamento schema:", ex.Message); + //DatabaseService.ErrorMessage = $"Errore nel caricamento dello schema: {ex.Message}"; + schemaInfo = null; + } + finally + { + isLoading = false; + } + } + + private void SelectTable(string tableName) + { + selectedTable = tableName; + } + + private void FilterTables(ChangeEventArgs e) + { + tableFilter = e.Value?.ToString() ?? ""; + + if (string.IsNullOrWhiteSpace(tableFilter)) + { + filteredTables = schemaInfo; + } + else + { + filteredTables = schemaInfo + .Where(t => t.Key.Contains(tableFilter, StringComparison.OrdinalIgnoreCase)) + .ToDictionary(t => t.Key, t => t.Value); + } + } + + private async Task RefreshSchema() + { + await LoadDatabaseSchema(); + } + + private void NavigateToConnection() + { + NavigationManager.NavigateTo("/database-connection"); + } +} \ No newline at end of file diff --git a/Data_Coupler/Shared/NavMenu.razor b/Data_Coupler/Shared/NavMenu.razor index fa0baee..a41290d 100644 --- a/Data_Coupler/Shared/NavMenu.razor +++ b/Data_Coupler/Shared/NavMenu.razor @@ -29,6 +29,11 @@ Database +