[Feature] Implementazione completa supporto ODBC

- Aggiunta persistenza campi ODBC (OdbcDsnName, OdbcMode) in CredentialEntity
- Creata migration EF Core per nuovi campi database
- Aggiornato mapping credenziali per caricare/salvare dati ODBC
- Creato OdbcDatabaseManager dedicato (bypass EF Core che non supporta ODBC)
- Aggiornato DataConnectionFactory per usare OdbcDatabaseManager con connessioni ODBC
- Fix auto-load DSN: sostituito @onchange con @bind-Value:after in dropdown tipo database
- Fix test connessione SAP HANA: rimossa query SELECT 1 che causava errori sintassi
- Implementati tutti i metodi IDatabaseManager in OdbcDatabaseManager
- Supporto completo per discovery schema, tabelle e query ODBC

Risolve problema DbContext non configurato per ODBC e abilita connessioni ODBC complete.
This commit is contained in:
Alessio Dal Santo
2026-02-02 18:24:44 +01:00
parent e7fb9a5cc7
commit 01f78466df
22 changed files with 3615 additions and 49 deletions
@@ -0,0 +1,593 @@
// <auto-generated />
using System;
using CredentialManager.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CredentialManager.Data.Migrations
{
[DbContext(typeof(CredentialDbContext))]
[Migration("20260202165251_AddOdbcFieldsToCredentialEntity")]
partial class AddOdbcFieldsToCredentialEntity
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.0");
modelBuilder.Entity("CredentialManager.Models.CredentialEntity", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AdditionalParameters")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<int>("CommandTimeout")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(30);
b.Property<string>("ConnectionString")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("CreatedBy")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("DatabaseName")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("DatabaseType")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<string>("EncryptedApiKey")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("EncryptedAuthToken")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("EncryptedPassword")
.HasColumnType("TEXT");
b.Property<string>("Headers")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<string>("Host")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<bool>("IgnoreSslErrors")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(false);
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("OdbcDsnName")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("OdbcMode")
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<int?>("Port")
.HasColumnType("INTEGER");
b.Property<string>("RestServiceType")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<int>("TimeoutSeconds")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(100);
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("TEXT");
b.Property<string>("Username")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DatabaseType");
b.HasIndex("IsActive");
b.HasIndex("Name")
.IsUnique();
b.HasIndex("Type");
b.ToTable("Credentials", (string)null);
});
modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
b.Property<string>("CreatedBy")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("DeletionAction")
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<string>("DeletionMarkField")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("DeletionMarkValue")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<int?>("DestinationCredentialId")
.HasColumnType("INTEGER");
b.Property<string>("DestinationEndpoint")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("DestinationSchema")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("DestinationTable")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("DestinationType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<string>("FieldMappingJson")
.HasMaxLength(4000)
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<DateTime?>("LastUsedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int?>("SourceCredentialId")
.HasColumnType("INTEGER");
b.Property<string>("SourceCustomQuery")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<string>("SourceDatabaseName")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("SourceFilePath")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("SourceKeyField")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("SourceSchema")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("SourceTable")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("SourceType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<bool>("SyncDeletions")
.HasColumnType("INTEGER");
b.Property<bool>("UseRecordAssociations")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("DestinationCredentialId");
b.HasIndex("DestinationType");
b.HasIndex("IsActive");
b.HasIndex("LastUsedAt");
b.HasIndex("Name")
.IsUnique();
b.HasIndex("SourceCredentialId");
b.HasIndex("SourceType");
b.ToTable("DataCouplerProfiles", (string)null);
});
modelBuilder.Entity("CredentialManager.Models.KeyAssociation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AdditionalInfo")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Data_Hash")
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<DateTime?>("DeletedAt")
.HasColumnType("TEXT");
b.Property<bool>("DeletionSynced")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DeletionSyncedAt")
.HasColumnType("TEXT");
b.Property<string>("DestinationEntity")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("DestinationId")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("DestinationKeyField")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasDefaultValue(true);
b.Property<bool>("IsSourceDeleted")
.HasColumnType("INTEGER");
b.Property<string>("KeyValue")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime?>("LastVerifiedAt")
.HasColumnType("TEXT");
b.Property<string>("MappedDestinationField")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("RestCredentialName")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SourceKeyField")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("SourcesInfo")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedAt");
b.HasIndex("DestinationEntity");
b.HasIndex("IsActive");
b.HasIndex("KeyValue")
.HasDatabaseName("IX_KeyAssociations_KeyValue");
b.HasIndex("LastVerifiedAt");
b.HasIndex("RestCredentialName");
b.HasIndex("KeyValue", "DestinationEntity", "RestCredentialName")
.IsUnique()
.HasDatabaseName("IX_KeyAssociations_Unique");
b.ToTable("KeyAssociations", (string)null);
});
modelBuilder.Entity("CredentialManager.Models.ProfileSchedule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("CreatedBy")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("DailyTime")
.HasMaxLength(10)
.HasColumnType("TEXT");
b.Property<int?>("DayOfMonth")
.HasColumnType("INTEGER");
b.Property<int?>("DayOfWeek")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("DestinationDatabaseOverride")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<bool>("EnableDeletionSync")
.HasColumnType("INTEGER");
b.Property<int>("ExecutionCount")
.HasColumnType("INTEGER");
b.Property<string>("IntervalUnit")
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<int?>("IntervalValue")
.HasColumnType("INTEGER");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<bool>("IsEnabled")
.HasColumnType("INTEGER");
b.Property<string>("LastExecutionMessage")
.HasMaxLength(1000)
.HasColumnType("TEXT");
b.Property<int?>("LastExecutionRecordCount")
.HasColumnType("INTEGER");
b.Property<string>("LastExecutionStatus")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<DateTime?>("LastExecutionTime")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime?>("NextExecutionTime")
.HasColumnType("TEXT");
b.Property<int>("ProfileId")
.HasColumnType("INTEGER");
b.Property<string>("ScheduleType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<DateTime?>("ScheduledDateTime")
.HasColumnType("TEXT");
b.Property<string>("SourceDatabaseOverride")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime?>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ProfileId");
b.ToTable("ProfileSchedules");
});
modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AdditionalInfo")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("DestinationInfo")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("DestinationType")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<DateTime?>("EndTime")
.HasColumnType("TEXT");
b.Property<string>("ErrorDetails")
.HasMaxLength(5000)
.HasColumnType("TEXT");
b.Property<string>("Message")
.HasMaxLength(2000)
.HasColumnType("TEXT");
b.Property<int>("ProfileId")
.HasColumnType("INTEGER");
b.Property<string>("ProfileName")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<int>("RecordsProcessed")
.HasColumnType("INTEGER");
b.Property<int?>("RecordsWithErrors")
.HasColumnType("INTEGER");
b.Property<int>("ScheduleId")
.HasColumnType("INTEGER");
b.Property<string>("SourceInfo")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<string>("SourceType")
.HasMaxLength(50)
.HasColumnType("TEXT");
b.Property<DateTime>("StartTime")
.HasColumnType("TEXT");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<string>("TriggerType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<string>("TriggeredBy")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("ProfileId");
b.HasIndex("ScheduleId");
b.HasIndex("StartTime");
b.HasIndex("Status");
b.HasIndex("TriggerType");
b.ToTable("ScheduleExecutionHistories", (string)null);
});
modelBuilder.Entity("CredentialManager.Models.DataCouplerProfile", b =>
{
b.HasOne("CredentialManager.Models.CredentialEntity", "DestinationCredential")
.WithMany()
.HasForeignKey("DestinationCredentialId")
.OnDelete(DeleteBehavior.SetNull);
b.HasOne("CredentialManager.Models.CredentialEntity", "SourceCredential")
.WithMany()
.HasForeignKey("SourceCredentialId")
.OnDelete(DeleteBehavior.SetNull);
b.Navigation("DestinationCredential");
b.Navigation("SourceCredential");
});
modelBuilder.Entity("CredentialManager.Models.ProfileSchedule", b =>
{
b.HasOne("CredentialManager.Models.DataCouplerProfile", "Profile")
.WithMany()
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Profile");
});
modelBuilder.Entity("CredentialManager.Models.ScheduleExecutionHistory", b =>
{
b.HasOne("CredentialManager.Models.ProfileSchedule", "Schedule")
.WithMany()
.HasForeignKey("ScheduleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Schedule");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CredentialManager.Data.Migrations
{
/// <inheritdoc />
public partial class AddOdbcFieldsToCredentialEntity : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "OdbcDsnName",
table: "Credentials",
type: "TEXT",
maxLength: 100,
nullable: true);
migrationBuilder.AddColumn<string>(
name: "OdbcMode",
table: "Credentials",
type: "TEXT",
maxLength: 20,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "OdbcDsnName",
table: "Credentials");
migrationBuilder.DropColumn(
name: "OdbcMode",
table: "Credentials");
}
}
}
@@ -85,6 +85,14 @@ namespace CredentialManager.Migrations
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("OdbcDsnName")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("OdbcMode")
.HasMaxLength(20)
.HasColumnType("TEXT");
b.Property<int?>("Port")
.HasColumnType("INTEGER");
@@ -61,6 +61,13 @@ public class CredentialEntity
[MaxLength(2000)]
public string? AdditionalParameters { get; set; } // JSON per parametri aggiuntivi
// ODBC specific fields
[MaxLength(100)]
public string? OdbcDsnName { get; set; } // Nome del DSN ODBC configurato
[MaxLength(20)]
public string? OdbcMode { get; set; } // Dsn o Custom (OdbcConnectionMode enum)
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public DateTime? UpdatedAt { get; set; }
+91 -1
View File
@@ -33,7 +33,24 @@ public enum DatabaseType
Oracle,
Sqlite,
DB2,
SapHana
SapHana,
Odbc
}
/// <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>
@@ -52,6 +69,10 @@ public class DatabaseCredential
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>
@@ -148,6 +169,7 @@ public static class ConnectionStringBuilder
DatabaseType.Sqlite => BuildSqliteConnectionString(credential),
DatabaseType.DB2 => BuildDb2ConnectionString(credential),
DatabaseType.SapHana => BuildSapHanaConnectionString(credential),
DatabaseType.Odbc => BuildOdbcConnectionString(credential),
_ => throw new NotSupportedException($"Database type {credential.DatabaseType} not supported")
};
} private static string BuildSqlServerConnectionString(DatabaseCredential credential)
@@ -275,6 +297,74 @@ public static class ConnectionStringBuilder
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 void AddAdditionalParameters(List<string> builder, Dictionary<string, string>? additionalParams)
{
if (additionalParams != null)
@@ -89,6 +89,8 @@ public class CredentialService : ICredentialService
AdditionalParameters = credential.AdditionalParameters != null
? JsonSerializer.Serialize(credential.AdditionalParameters)
: null,
OdbcDsnName = credential.OdbcDsnName,
OdbcMode = credential.OdbcMode.ToString(),
CreatedAt = DateTime.UtcNow,
CreatedBy = Environment.UserName
};
@@ -110,6 +112,8 @@ public class CredentialService : ICredentialService
existing.CommandTimeout = entity.CommandTimeout;
existing.IgnoreSslErrors = entity.IgnoreSslErrors;
existing.AdditionalParameters = entity.AdditionalParameters;
existing.OdbcDsnName = entity.OdbcDsnName;
existing.OdbcMode = entity.OdbcMode;
existing.UpdatedAt = DateTime.UtcNow;
_context.Credentials.Update(existing);
@@ -695,7 +699,11 @@ public class CredentialService : ICredentialService
Password = DecryptSafely(entity.EncryptedPassword, entity.Name, "password"),
ConnectionString = entity.ConnectionString,
CommandTimeout = entity.CommandTimeout,
IgnoreSslErrors = entity.IgnoreSslErrors
IgnoreSslErrors = entity.IgnoreSslErrors,
OdbcDsnName = entity.OdbcDsnName,
OdbcMode = !string.IsNullOrEmpty(entity.OdbcMode) && Enum.TryParse<OdbcConnectionMode>(entity.OdbcMode, out var odbcMode)
? odbcMode
: OdbcConnectionMode.Dsn
};
if (!string.IsNullOrEmpty(entity.AdditionalParameters))
@@ -0,0 +1,182 @@
using Microsoft.Win32;
using Microsoft.Extensions.Logging;
namespace CredentialManager.Services;
/// <summary>
/// Informazioni su un DSN ODBC
/// </summary>
public class OdbcDsnInfo
{
public string Name { get; set; } = string.Empty;
public string Driver { get; set; } = string.Empty;
public string? Description { get; set; }
public bool IsUserDsn { get; set; } // true = User DSN, false = System DSN
public Dictionary<string, string> Properties { get; set; } = new();
}
/// <summary>
/// Interfaccia per il servizio di discovery DSN ODBC
/// </summary>
public interface IOdbcDsnDiscoveryService
{
/// <summary>
/// Ottiene tutti i DSN ODBC configurati (sia User che System)
/// </summary>
List<OdbcDsnInfo> GetAllDsn();
/// <summary>
/// Ottiene solo i DSN utente
/// </summary>
List<OdbcDsnInfo> GetUserDsn();
/// <summary>
/// Ottiene solo i DSN di sistema
/// </summary>
List<OdbcDsnInfo> GetSystemDsn();
/// <summary>
/// Ottiene i dettagli di un DSN specifico
/// </summary>
OdbcDsnInfo? GetDsnDetails(string dsnName, bool isUserDsn = true);
/// <summary>
/// Ottiene la lista dei driver ODBC installati
/// </summary>
List<string> GetInstalledDrivers();
}
/// <summary>
/// Servizio per la scoperta e lettura dei DSN ODBC configurati sul sistema
/// </summary>
public class OdbcDsnDiscoveryService : IOdbcDsnDiscoveryService
{
private readonly ILogger<OdbcDsnDiscoveryService> _logger;
// Percorsi del registro di Windows per ODBC
private const string USER_DSN_PATH = @"SOFTWARE\ODBC\ODBC.INI\ODBC Data Sources";
private const string SYSTEM_DSN_PATH = @"SOFTWARE\ODBC\ODBC.INI\ODBC Data Sources";
private const string USER_DSN_DETAILS_PATH = @"SOFTWARE\ODBC\ODBC.INI\";
private const string SYSTEM_DSN_DETAILS_PATH = @"SOFTWARE\ODBC\ODBC.INI\";
private const string DRIVERS_PATH = @"SOFTWARE\ODBC\ODBCINST.INI\ODBC Drivers";
public OdbcDsnDiscoveryService(ILogger<OdbcDsnDiscoveryService> logger)
{
_logger = logger;
}
public List<OdbcDsnInfo> GetAllDsn()
{
var allDsn = new List<OdbcDsnInfo>();
allDsn.AddRange(GetUserDsn());
allDsn.AddRange(GetSystemDsn());
return allDsn;
}
public List<OdbcDsnInfo> GetUserDsn()
{
return GetDsnFromRegistry(Registry.CurrentUser, USER_DSN_PATH, USER_DSN_DETAILS_PATH, true);
}
public List<OdbcDsnInfo> GetSystemDsn()
{
return GetDsnFromRegistry(Registry.LocalMachine, SYSTEM_DSN_PATH, SYSTEM_DSN_DETAILS_PATH, false);
}
public OdbcDsnInfo? GetDsnDetails(string dsnName, bool isUserDsn = true)
{
var allDsn = isUserDsn ? GetUserDsn() : GetSystemDsn();
return allDsn.FirstOrDefault(d => d.Name.Equals(dsnName, StringComparison.OrdinalIgnoreCase));
}
public List<string> GetInstalledDrivers()
{
var drivers = new List<string>();
try
{
using var key = Registry.LocalMachine.OpenSubKey(DRIVERS_PATH);
if (key != null)
{
foreach (var driverName in key.GetValueNames())
{
var value = key.GetValue(driverName)?.ToString();
if (value == "Installed")
{
drivers.Add(driverName);
}
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella lettura dei driver ODBC dal registro");
}
return drivers.OrderBy(d => d).ToList();
}
private List<OdbcDsnInfo> GetDsnFromRegistry(RegistryKey rootKey, string dsnPath, string detailsPath, bool isUserDsn)
{
var dsnList = new List<OdbcDsnInfo>();
try
{
using var dsnKey = rootKey.OpenSubKey(dsnPath);
if (dsnKey == null)
{
_logger.LogWarning("Chiave registro ODBC non trovata: {Path}", dsnPath);
return dsnList;
}
foreach (var dsnName in dsnKey.GetValueNames())
{
try
{
var driver = dsnKey.GetValue(dsnName)?.ToString();
if (string.IsNullOrEmpty(driver))
continue;
var dsnInfo = new OdbcDsnInfo
{
Name = dsnName,
Driver = driver,
IsUserDsn = isUserDsn
};
// Leggi i dettagli del DSN
using var detailKey = rootKey.OpenSubKey(detailsPath + dsnName);
if (detailKey != null)
{
foreach (var valueName in detailKey.GetValueNames())
{
var value = detailKey.GetValue(valueName)?.ToString();
if (!string.IsNullOrEmpty(value))
{
dsnInfo.Properties[valueName] = value;
// Popola proprietà comuni
if (valueName.Equals("Description", StringComparison.OrdinalIgnoreCase))
dsnInfo.Description = value;
}
}
}
dsnList.Add(dsnInfo);
_logger.LogDebug("DSN trovato: {Name} ({Driver}) - Type: {Type}",
dsnName, driver, isUserDsn ? "User" : "System");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Errore nella lettura del DSN: {DsnName}", dsnName);
}
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nella lettura dei DSN ODBC dal registro");
}
return dsnList;
}
}
Binary file not shown.