Questo esempio mostra come creare un extension method AddDatabaseConfiguration, per .NET 10, che crea un Custom Configuration Provider per legge i valori di configurazione da una tabella SQL Server.

Creazione Tabella

Per prima cosa creare una tabella AppSettings per salvare le configurazione:

T-SQL: AppSettings

CREATE TABLE AppSettings (
    [Key] NVARCHAR(255) NOT NULL PRIMARY KEY,
    [Value] NVARCHAR(MAX) NULL,
    [IsEncrypted] BIT NOT NULL DEFAULT 0
)
Esempio tabella
Esempio tabella
che corrisponde ad un appsettings.json come questo:

JSON: appsettings.json

{
  "AppOptions": {
    "MsSql": {
      "ConnectionString": "Server=localhost;Database=TestDB;Integrated Security=true;TrustServerCertificate=True;"
    },
    /* i seguenti valori sono letti da DB */
    "Enabled": true,
    "SecretPassword": "UIN/qj2RJvZSxEfrgpFbbwVKPo4z74kBXX+2O4bNK8/FFhed8xw/asC4/sfRYU50"
  },
  ...
}

Classi necessarie

Per gestire questa tabella si parte creando un ConfigurationProvider che fa l'override del metodo Load():

C#: MsSqlDatabaseConfigurationProvider

using Microsoft.Data.SqlClient;

public class MsSqlDatabaseConfigurationProvider(string connectionString) : ConfigurationProvider
{
    public override void Load()
    {
        Dictionary<string, string?> data = new(StringComparer.OrdinalIgnoreCase);

        using SqlConnection cnn = new(connectionString);
        cnn.Open();
        using SqlCommand cmd = new("SELECT [Key], [Value], [IsEncrypted] FROM AppSettings", cnn);
        using SqlDataReader dr = cmd.ExecuteReader();
        while (dr.Read())
        {
            string key = dr.GetString(0);
            string? value = dr.IsDBNull(1) ? null : dr.GetString(1);
            bool isEncrypted = dr.IsDBNull(2) ? false : dr.GetBoolean(2);
            if (isEncrypted && value != null)
            {
                // Decrypt the value if it is encrypted
                string decryptedValue = CryptoHelper.Decrypt(value.Trim());
                data[key] = decryptedValue;
            }
            else
            {
                data[key] = value;
            }
        }
        Data = data;
    }
}
Da notare che vengono gestiti anche valori stringa cryptati tramite il flag IsEncrypted e il metodo CryptoHelper.Decrypt(value.Trim()).

Per gestire questo serve la classe CryptoHelper.cs

In seguito va creato un IConfigurationSource che usa il provider precedente:

C#: MsSqlDatabaseConfigurationSource

public class MsSqlDatabaseConfigurationSource(string connectionString) : IConfigurationSource
{
    public IConfigurationProvider Build(IConfigurationBuilder builder)
        => new MsSqlDatabaseConfigurationProvider(connectionString);
}

in ultimo l'extension method da usare in Program.cs:

C#: MsSqlDatabaseConfigurationExtension

public static class MsSqlDatabaseConfigurationExtension
{
    /// <summary>
    /// Adds a configuration provider that reads configuration values from a SQL Server database.
    /// </summary>
    /// <param name="builder"></param>
    /// <param name="connectionString"></param>
    /// <returns></returns>
    public static IConfigurationBuilder AddDatabaseConfiguration(this IConfigurationBuilder builder, string connectionString)
        => builder.Add(new MsSqlDatabaseConfigurationSource(connectionString));

    public static void CreateTableIfNotExists(string connectionString)
    {
        using SqlConnection cnn = new(connectionString);
        cnn.Open();
        using SqlCommand cmd = new("""
            IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='AppSettings' AND xtype='U')
            CREATE TABLE AppSettings (
                [Key] NVARCHAR(255) NOT NULL PRIMARY KEY,
                [Value] NVARCHAR(MAX) NULL,
                [IsEncrypted] BIT NOT NULL DEFAULT 0
            )
            """, cnn);
        cmd.ExecuteNonQuery();
    }

    public static string EncryptValue(string value) => CryptoHelper.Encrypt(value);
}

Utilizzo

In una WebApplication, l'extension method AddDatabaseConfiguration va inserito subito dopo CreateBuilder() in Program.cs:

C#: AddDatabaseConfiguration

using WebApplication1.Extensions;

var builder = WebApplication.CreateBuilder(args);

// gestisco la configurazione su DB
string connectionString = builder.Configuration.GetValue<string>("AppOptions:MsSql:ConnectionString") 
    ?? throw new InvalidOperationException("ConnectionString nella sezione AppOptions:MsSql mancante.");

builder.Configuration.AddDatabaseConfiguration(connectionString);

// se serve mappo la configurazione su una classe AppOptions per avere una configurazione tipizzata
AppOptions options = builder.Configuration.GetSection(AppOptions.SECTION_NAME).Get<AppOptions>()
      ?? throw new InvalidOperationException("Sezione AppSettings mancante.");

Console.WriteLine($"Pwd: {options.SecretPassword}");
...
ATTENZIONE: i valori letti da Database SOVRASCRIVONO quelli dei provider di configurazione precedenti, come il file appsetting.json, variabili di ambiente o user secret.

Esempio di classe AppOptions:

C#: AppOptions

public class AppOptions
{
    public static string SECTION_NAME = "AppOptions";

    public bool Enabled { get; set; }
    public string SecretPassword { get; set; } = string.Empty;
    public MsSqlOptions? MsSql { get; set; }
}

public class MsSqlOptions
{
    public string? ConnectionString { get; set; }
}

Creazione stringa criptata

La stringa criptata può essere generata da un testo in chiaro chiamando l'eseguibile della WebApplication in questo modo:

C#:

.\WebApplication1.exe encrypt "prova aaa"
che darà un output simile a questo

Text

STARTING COMMAND 'encrypt' with parameter 'prova aaa'...
 Encrypted: 'UIN/qj2RJvZSxEfrgpFbbwVKPo4z74kBXX+2O4bNK8/FFhed8xw/asC4/sfRYU50'
STOPPING
questo comportamento lo si ottiene modificare leggermente il Program.cs:

C#

var builder = WebApplication.CreateBuilder(args);

if (args.Length > 1)
{
    string cmd = args[0].ToLowerInvariant();
    string? param = args[1];
    Console.WriteLine($"STARTING COMMAND '{cmd}' with parameter '{param}'...");
    if (cmd == "encrypt")
    {
        // solo per gestire la generazione della password chiamando l'exe
        // WebApplicaion1.exe encrypt "testo da criptare"
        Console.WriteLine($" Encrypted: '{MsSqlDatabaseConfigurationExtension.EncryptValue(param!)}'");

    }
    else if (cmd == "create" && param.Equals("table", StringComparison.OrdinalIgnoreCase))
    {
        // creazione della tabella AppSettings se non esiste, chiamando l'exe
        // WebApplicaion1.exe create table
        Console.WriteLine(" CREATING TABLE...");
        MsSqlDatabaseConfigurationExtension.CreateTableIfNotExists(builder.Configuration.GetValue<string>("AppOptions:MsSql:ConnectionString")!);
    }
    else
    {
        Console.WriteLine($" Parameter '{cmd}' is not recognized\r\n");
    }
    Console.WriteLine("STOPPING");
    return;
}
// il resto della configurazione delle WebApplication

Si può anche creare la tabella AppSettings con il comando:

PowerShell

.\WebApplication1.exe create table

Miglioramenti

Se serve, e se l'applicantion pool della WebApplication ha i permessi di creazione tabella, si può aggiungere un extension method per verificare l'esistenza della tabella e nel caso crearla.

La classe MsSqlDatabaseConfigurationExtension, con l'aggiunta del metodo EnsureAndAddDatabaseConfiguration, diventa:

C#: MsSqlDatabaseConfigurationExtension

public static class MsSqlDatabaseConfigurationExtension
{
    /// <summary>
    /// Ensures that the AppSettings table exists in the database and 
    /// adds a configuration provider that reads configuration values from a SQL Server database.
    /// </summary>
    /// <param name="builder"></param>
    /// <param name="connectionString"></param>
    /// <returns></returns>
    public static IConfigurationBuilder EnsureAndAddDatabaseConfiguration(this IConfigurationBuilder builder, string connectionString)
    {
        CreateTableIfNotExists(connectionString);
        return builder.Add(new MsSqlDatabaseConfigurationSource(connectionString));
    }
    // contenute esistente ...
}
e nel Program.cs diventa:

C#: Program.cs - EnsureAndAddDatabaseConfiguration

// gestisco la configurazione su DB
string connectionString = builder.Configuration.GetValue<string>("AppOptions:MsSql:ConnectionString") 
    ?? throw new InvalidOperationException("ConnectionString nella sezione AppOptions:MsSql mancante.");

builder.Configuration.EnsureAndAddDatabaseConfiguration(connectionString);
Tags:
C#243 .NET71 SQL Server101 T-SQL67
Potrebbe interessarti anche: