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(); // Verifica se la connection string specifica già un database var connectionBuilder = new SqlConnectionStringBuilder(connectionString); var currentDatabase = connectionBuilder.InitialCatalog; if (string.IsNullOrEmpty(currentDatabase)) { // Nessun database specificato - restituisce dizionario vuoto per indicare // che è necessaria la selezione del database Console.WriteLine("Nessun database specificato nella connection string. È richiesta la selezione del database."); return new Dictionary>(); } Console.WriteLine($"Analizzando database: {currentDatabase}"); // Prima verifichiamo se ci sono tabelle utente nel database specificato string testSql = "SELECT COUNT(*) FROM sys.tables WHERE is_ms_shipped = 0"; using (var testCommand = new SqlCommand(testSql, connection)) { var scalarResult = await testCommand.ExecuteScalarAsync(); var tableCount = scalarResult != null ? (int)scalarResult : 0; Console.WriteLine($"Trovate {tableCount} tabelle utente nel database {currentDatabase}"); if (tableCount == 0) { return new Dictionary>(); // Restituisce dizionario vuoto } } // Se ci sono tabelle, procediamo con la query completa // 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; } /// /// Ottiene la lista dei database disponibili sul server SQL Server /// public async Task> GetAvailableDatabasesAsync(string connectionString) { var databases = new List(); try { // Crea una connection string per il server (rimuove il database specifico) var connectionBuilder = new SqlConnectionStringBuilder(connectionString); connectionBuilder.InitialCatalog = ""; // Rimuove il database specifico var serverConnectionString = connectionBuilder.ConnectionString; using (var connection = new SqlConnection(serverConnectionString)) { await connection.OpenAsync(); // Query per ottenere i database disponibili (esclude quelli di sistema) string sql = @" SELECT name FROM sys.databases WHERE state_desc = 'ONLINE' AND name NOT IN ('master', 'tempdb', 'model', 'msdb', 'distribution') AND HAS_DBACCESS(name) = 1 ORDER BY name"; using (var command = new SqlCommand(sql, connection)) { using (var reader = await command.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { databases.Add(reader.GetString(0)); } } } } } catch (Exception ex) { Console.WriteLine($"Errore nel recupero della lista dei database: {ex.Message}"); throw; } return databases; } /// /// Ottiene solo la lista dei nomi delle tabelle disponibili (senza dettagli delle colonne) /// public async Task> GetTableNamesAsync(string connectionString) { var tableNames = new List(); try { using (var connection = new SqlConnection(connectionString)) { await connection.OpenAsync(); // Verifica se la connection string specifica già un database var connectionBuilder = new SqlConnectionStringBuilder(connectionString); var currentDatabase = connectionBuilder.InitialCatalog; if (string.IsNullOrEmpty(currentDatabase)) { Console.WriteLine("Nessun database specificato nella connection string per ottenere i nomi delle tabelle."); return new List(); } Console.WriteLine($"Recuperando nomi tabelle dal database: {currentDatabase}"); // Query semplice per ottenere solo i nomi delle tabelle string sql = @" SELECT SCHEMA_NAME(t.schema_id) + '.' + t.name AS TableName FROM sys.tables t WHERE t.is_ms_shipped = 0 ORDER BY TableName"; using (var command = new SqlCommand(sql, connection)) { using (var reader = await command.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { tableNames.Add(reader.GetString(0)); } } } } } catch (Exception ex) { Console.WriteLine($"Errore nel recupero dei nomi delle tabelle: {ex.Message}"); throw; } return tableNames; } /// /// Ottiene i dettagli delle colonne per una specifica tabella /// public async Task> GetTableSchemaAsync(string connectionString, string tableName) { var columns = new List(); try { using (var connection = new SqlConnection(connectionString)) { await connection.OpenAsync(); // Verifica se la connection string specifica già un database var connectionBuilder = new SqlConnectionStringBuilder(connectionString); var currentDatabase = connectionBuilder.InitialCatalog; if (string.IsNullOrEmpty(currentDatabase)) { Console.WriteLine("Nessun database specificato nella connection string per ottenere lo schema della tabella."); return new List(); } Console.WriteLine($"Recuperando schema della tabella {tableName} dal database: {currentDatabase}"); // Separa schema e nome tabella string schemaName = "dbo"; string tableNameOnly = tableName; if (tableName.Contains('.')) { var parts = tableName.Split('.'); schemaName = parts[0]; tableNameOnly = parts[1]; } // Query per ottenere i dettagli delle colonne di una specifica tabella string sql = @" SELECT 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 AND SCHEMA_NAME(t.schema_id) = @schemaName AND t.name = @tableName ORDER BY c.column_id"; using (var command = new SqlCommand(sql, connection)) { // Aggiungi parametri per evitare SQL injection var schemaParam = command.CreateParameter(); schemaParam.ParameterName = "@schemaName"; schemaParam.Value = schemaName; command.Parameters.Add(schemaParam); var tableParam = command.CreateParameter(); tableParam.ParameterName = "@tableName"; tableParam.Value = tableNameOnly; command.Parameters.Add(tableParam); using (var reader = await command.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { // Formato del tipo di dati con precisione e scala per tipi numerici o lunghezza per tipi stringa string dataType = reader.GetString(1); int maxLength = reader.GetInt16(2); byte precision = reader.GetByte(3); byte scale = reader.GetByte(4); // Formattazione tipo di dati per SQL Server string formattedDataType = FormatSqlServerDataType(dataType, maxLength, precision, scale); var columnInfo = new DbColumnInfo { Name = reader.GetString(0), DataType = formattedDataType, IsNullable = reader.GetBoolean(5), IsPrimaryKey = reader.GetInt32(6) == 1, IsForeignKey = reader.GetInt32(7) == 1, ReferencedTable = reader.IsDBNull(8) ? null : reader.GetString(8), ReferencedColumn = reader.IsDBNull(9) ? null : reader.GetString(9) }; columns.Add(columnInfo); } } } } } catch (Exception ex) { Console.WriteLine($"Errore nel recupero dello schema della tabella {tableName}: {ex.Message}"); throw; } return columns; } 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; } }