Oltre appsettings.json — Guida pratica alla gestione della configurazione nelle applicazioni aziendali Windows (impostazioni per ambiente, segreti e percorsi di scrittura)

· · CSharp, .NET, appsettings.json, IConfiguration, IOptions, Generic Host, Gestione della configurazione, Sviluppo Windows, Consulenza tecnica

«Voglio una stringa di connessione diversa per ambiente». «Voglio salvare le preferenze di visualizzazione di ogni utente». «Abbiamo finito per scrivere l’API key direttamente in appsettings.json». Nella gestione della configurazione di un’applicazione aziendale Windows, prima o poi tutti arrivano a inserire un appsettings.json e leggerlo tramite IConfiguration. Ma oltre quel punto — impostazioni per ambiente, posizione dei dati scrivibili, segreti e modifiche a runtime — c’è un’area che sorprendentemente spesso arriva in produzione senza essere stata realmente ordinata.

Questo articolo affronta la base IConfiguration e la stratificazione dei provider, le impostazioni per ambiente, la ricezione tipizzata tramite il pattern IOptions, la posizione delle impostazioni modificabili, i segreti, i limiti reali della modifica a runtime e infine una mappa di migrazione da app.config/Settings.settings, nell’ordine in cui queste decisioni creano difficoltà nella pratica.

1. La conclusione prima di tutto

Le decisioni di gestione della configurazione iniziano scegliendo una “posizione” per ogni “tipo di impostazione”. Ecco prima una tabella generale.

Tipo di impostazione Esempio Prima scelta Motivo
Valori predefiniti dell’applicazione Livello di log predefinito, parametri UI appsettings.json Distribuiti con l’output della build; valori comuni non dipendenti dall’ambiente
Impostazioni per ambiente Stringhe di connessione e endpoint API diversi tra staging e produzione appsettings.{Environment}.json + variabili d’ambiente Consente di usare direttamente l’ordine di override predefinito
Impostazioni per utente Ultima cartella aperta, posizione della finestra, preferenze personali File personalizzato sotto %LOCALAPPDATA% (o %APPDATA%) Serve una posizione in cui ciascun utente possa scrivere separatamente
Impostazioni per computer, condivise da tutti Numero di porta COM di un dispositivo, indirizzo del server licenze File personalizzato sotto %ProgramData% Impostato una volta dall’amministratore e condiviso tra utenti
Segreti Password di stringhe di connessione, API key, token File protetto da DPAPI (produzione), user-secrets (solo sviluppo) Mai testo in chiaro; non riutilizzare in produzione un meccanismo solo di sviluppo
Valori che cambiano a runtime Feature flag, variazione dinamica del livello di log appsettings.json (reloadOnChange) + IOptionsMonitor Riservarlo ai soli valori che si vuole davvero applicare senza riavvio

Con questa tabella come base, ecco i punti essenziali.

  • La precedenza della configurazione segue una sola regola: vince il provider aggiunto più tardi. Per impostazione predefinita i provider vengono caricati nell’ordine appsettings.jsonappsettings.{Environment}.json → user secrets (solo sviluppo) → variabili d’ambiente → argomenti della riga di comando; quando la stessa chiave appare in più fonti, il valore letto dopo sovrascrive quello precedente. Ricordare quest’ordine spiega la maggior parte dei casi «l’ho scritto nel JSON ma non ha effetto». 1
  • Usando Host.CreateApplicationBuilder, l’intera stratificazione è predisposta per impostazione predefinita: non occorre costruirla a mano. Anche app desktop WinForms/WPF ottengono gli stessi vantaggi adottando Generic Host. 23
  • Cambiare ambiente con DOTNET_ENVIRONMENT (o ASPNETCORE_ENVIRONMENT). Nelle app basate su WebApplication, DOTNET_ENVIRONMENT ha priorità e, se non è impostata nessuna delle due, il valore predefinito è Production. Per applicazioni desktop e servizi Windows bisogna progettare in anticipo chi passa questa variabile al processo e come. 4
  • Non ricevere le impostazioni come semplice DTO: riceverle in forma tipizzata tramite la famiglia IOptions<T>. Usare IOptions<T> se basta calcolare il valore una volta all’avvio e IOptionsMonitor<T> se le modifiche devono essere riflesse senza riavvio. Usarli senza comprenderne la differenza porta a entrambi gli incidenti: valori che cambiano quando non dovrebbero e valori che non cambiano quando dovrebbero. 5
  • Far fallire una configurazione errata all’avvio, non a runtime. Combinando ValidateDataAnnotations() e ValidateOnStart(), un errore di configurazione diventa un’eccezione immediata all’avvio anziché un NullReferenceException alle due di notte in produzione. 6
  • Non mettere mai segreti in chiaro in appsettings.json. In sviluppo usare user-secrets e in produzione DPAPI (ProtectedData) o Credential Manager. La documentazione ufficiale dichiara esplicitamente che user-secrets non è cifrato ed è un meccanismo per il solo sviluppo. 78

2. Le fondamenta della configurazione .NET: IConfiguration e provider

Il sistema di configurazione .NET si basa sul caricamento stratificato di più configuration provider dietro l’aspetto di un singolo archivio chiave-valore chiamato IConfiguration. Il vantaggio è unificare fonti con caratteristiche molto diverse — JSON, variabili d’ambiente, argomenti da riga di comando, INI, XML, raccolte in memoria e altro — attraverso la stessa interfaccia IConfiguration. 9

I provider si sovrappongono secondo una semplice regola: vince quello aggiunto più tardi. Quando una chiave esiste in più provider, è effettivo il valore dell’ultimo. 1

Host.CreateApplicationBuilder(args) predispone i provider nel seguente ordine (i numeri più alti hanno maggiore priorità, cioè sono aggiunti dopo). 2

  1. appsettings.json
  2. appsettings.{Environment}.json
  3. Secret Manager (solo ambiente di sviluppo)
  4. Variabili d’ambiente
  5. Argomenti della riga di comando

Questo ordine predefinito è lo stesso per console app, Worker Service e Windows Forms. Ecco un esempio minimo.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Per impostazione predefinita, appsettings.json / appsettings.{Environment}.json / variabili d'ambiente /
// argomenti della riga di comando sono già caricati nel seguente ordine di priorità
string? connectionString = builder.Configuration.GetConnectionString("Main");

using IHost host = builder.Build();
await host.RunAsync();

Anche un’app desktop Windows Forms può usare lo stesso HostApplicationBuilder aggiungendo il pacchetto Microsoft.Extensions.Hosting. Come spiegato nell’articolo “Che cos’è Generic Host?”, Generic Host gestisce insieme configurazione, DI e logging; un’app desktop non deve rinunciare ai vantaggi del sistema di configurazione. Per modelli concreti in app con elaborazione in background, vedere anche “Generic Host + BackgroundService in un’app desktop”.

Le variabili d’ambiente hanno una sottigliezza. Le host settings — content root, nome dell’ambiente e così via — sono lette da variabili con prefisso DOTNET_, ma tale prefisso non viene usato per le app settings, cioè i normali valori letti tramite IConfiguration. Le variabili da leggere come app settings entrano nell’ordine predefinito senza prefisso tramite AddEnvironmentVariables(). 10 Se si desidera un prefisso personalizzato, aggiungerlo esplicitamente, ad esempio builder.Configuration.AddEnvironmentVariables(prefix: "MyApp_").

using Microsoft.Extensions.Configuration;

// Poiché viene aggiunto dopo i provider predefiniti, ha la priorità più alta
builder.Configuration.AddEnvironmentVariables(prefix: "MyApp_");

3. Impostazioni per ambiente: appsettings.{Environment}.json

Quando stringhe di connessione o endpoint variano per ambiente, usare file come appsettings.Development.json, appsettings.Staging.json e appsettings.Production.json. Il punto importante è che questi file sono trattati come diff che sovrascrivono il appsettings.json di base. Non è necessario riscrivere ogni valore: bastano quelli che cambiano per ambiente. 1

Il nome dell’ambiente è determinato dalla variabile DOTNET_ENVIRONMENT o ASPNETCORE_ENVIRONMENT. Con WebApplication, DOTNET_ENVIRONMENT ha priorità su ASPNETCORE_ENVIRONMENT e, se non è impostata nessuna delle due, il valore predefinito è Production. I nomi delle variabili d’ambiente in Windows non sono sensibili alle maiuscole, mentre in Linux lo sono; se è prevista la containerizzazione, è più sicuro mantenere una capitalizzazione coerente. 4

La difficoltà è che per app desktop e servizi Windows, «come passare la variabile d’ambiente» diventa un lavoro a sé. Meccanismi di sviluppo come launchSettings.json di ASP.NET Core sono usati solo localmente, quindi in produzione occorre un modo separato per cambiare ambiente. I tre approcci principali sono questi.

  • Impostarla come variabile per l’intera macchina. Persistendola, ad esempio, con setx DOTNET_ENVIRONMENT Production /M, viene ereditata da ogni processo in esecuzione sul computer. Non è adatta quando sulla stessa macchina devono convivere applicazioni di ambienti diversi.
  • Passarla al processo di avvio del servizio Windows. Un servizio residente viene avviato in un contesto diverso dalla sessione dell’utente interattivo, quindi le variabili con scope utente non vengono ereditate. Per account di esecuzione e separazione delle sessioni, vedere “Come creare e gestire servizi Windows”. Sono configurazioni pratiche una variabile di macchina oppure un sottile batch wrapper che esegue SET prima di avviare il binario reale.
  • Con Task Scheduler è più affidabile passare un argomento della riga di comando. La scheda Action non offre una UI per impostare direttamente variabili d’ambiente; passare --environment Production e leggerlo con AddCommandLine(args) riduce gli errori. Le particolarità dell’account di esecuzione e del tipo di accesso di Task Scheduler sono trattate in “Attività dell’Utilità di pianificazione che non partono o terminano con 0x1”.
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

var options = new HostApplicationBuilderSettings
{
    Args = args,
    // Per i percorsi di esecuzione in cui non vengono passate variabili d'ambiente (ad esempio l'Utilità di pianificazione),
    // consentire anche il cambio di ambiente dagli argomenti della riga di comando
};
HostApplicationBuilder builder = Host.CreateApplicationBuilder(options);

Console.WriteLine($"Ambiente corrente: {builder.Environment.EnvironmentName}");

4. Il pattern Options: ricevere le impostazioni come tipi

Leggere la configurazione con chiavi stringa come IConfiguration["Key:SubKey"] ha una debolezza: un refuso non viene rilevato e, quando l’annidamento è profondo, diventa difficile seguire il flusso. In pratica l’approccio base è l’options pattern: associare le impostazioni a un POCO e riceverle come tipo.

public sealed class ExternalApiOptions
{
    public const string SectionName = "ExternalApi";

    public required string BaseUrl { get; set; }
    public required string ApiKey { get; set; }
    public int TimeoutSeconds { get; set; } = 30;
}

Registrazione e binding sono i seguenti.

using Microsoft.Extensions.DependencyInjection;

builder.Services
    .AddOptions<ExternalApiOptions>()
    .Bind(builder.Configuration.GetSection(ExternalApiOptions.SectionName));

Esistono tre interfacce per ricevere questi valori, ciascuna con caratteristiche diverse. 5

Interfaccia Lifetime di registrazione Riflette le modifiche di configurazione Uso principale
IOptions<T> Singleton Non le riflette: calcolato una volta all’avvio Impostazioni che non dovrebbero cambiare; opzione più semplice
IOptionsSnapshot<T> Scoped Ricalcolato a ogni ricostruzione dello scope Contesti con uno scope chiaro, come una richiesta web
IOptionsMonitor<T> Singleton Ottiene sempre il valore più recente e fornisce notifiche (OnChange) Contesti come servizi residenti in cui il cambiamento deve essere rilevato subito

Nelle app senza HTTP request scope — applicazioni desktop e servizi Windows — IOptionsSnapshot<T> si comporta di fatto come IOptions<T> finché non si crea esplicitamente lo scope. Se si desidera rilevare i cambiamenti, usare semplicemente IOptionsMonitor<T>: questa regola pratica elimina gran parte dell’incertezza.

I valori delle impostazioni — BaseUrl, ApiKey e timeout — vanno recuperati da IOptionsMonitor a ogni chiamata, ma l’HttpClient stesso va ottenuto da IHttpClientFactory e riutilizzato. Creare e disporre un HttpClient a ogni chiamata distrugge e ricrea ogni volta il pool interno di socket e connessioni, provocando esaurimento delle porte effimere con polling frequente o elaborazioni batch. La pratica standard è quindi: i valori possono cambiare, ma il riutilizzo delle connessioni resta a IHttpClientFactory.

using Microsoft.Extensions.Options;

public sealed class ExternalApiClient(
    IHttpClientFactory httpClientFactory,
    IOptionsMonitor<ExternalApiOptions> optionsMonitor)
{
    public async Task<string> FetchAsync(CancellationToken cancellationToken)
    {
        // Recupera il valore più recente a ogni chiamata. Viene applicato se il file di configurazione è stato aggiornato
        ExternalApiOptions current = optionsMonitor.CurrentValue;

        // Ottenere l'HttpClient tramite la factory e riutilizzarne il pool di connessioni interno
        HttpClient client = httpClientFactory.CreateClient(nameof(ExternalApiClient));

        using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(new Uri(current.BaseUrl), "status"));
        request.Headers.Add("X-Api-Key", current.ApiKey);

        using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(current.TimeoutSeconds));
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);

        using HttpResponseMessage response = await client.SendAsync(request, linkedCts.Token);
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync(cancellationToken);
    }
}

Sul lato chiamante registrare un client nominato, ad esempio builder.Services.AddHttpClient(nameof(ExternalApiClient));. I valori modificabili tramite configurazione, come BaseUrl e ApiKey, vengono aggiunti a HttpRequestMessage a ogni richiesta, mentre il costo di creare e disporre l’istanza HttpClient è lasciato alla factory: questa è la divisione delle responsabilità.

Quando un errore di configurazione si manifesta come eccezione a runtime, l’indagine si allunga. Combinando la validazione DataAnnotations con ValidateOnStart() è possibile progettare l’app in modo che una configurazione errata impedisca del tutto l’avvio. 6

using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.DependencyInjection;

public sealed class ExternalApiOptions
{
    public const string SectionName = "ExternalApi";

    [Required, Url]
    public required string BaseUrl { get; set; }

    [Required, MinLength(16)]
    public required string ApiKey { get; set; }

    [Range(1, 300)]
    public int TimeoutSeconds { get; set; } = 30;
}

builder.Services
    .AddOptions<ExternalApiOptions>()
    .Bind(builder.Configuration.GetSection(ExternalApiOptions.SectionName))
    .ValidateDataAnnotations()
    .ValidateOnStart(); // La convalida viene eseguita all'avvio dell'host (StartAsync/RunAsync), prima dell'avvio dei servizi ospitati

Senza ValidateOnStart(), la validazione viene rinviata al primo accesso effettivo all’option. La validazione stessa viene eseguita all’avvio dell’host (StartAsync/RunAsync), non subito dopo Build(); fare quindi attenzione ai test che chiamano solo Build() e mai RunAsync(), perché a quel punto la validazione non è ancora avvenuta. Per evitare il comportamento intermittente «l’app parte ma va in errore appena si apre la schermata che usa quell’impostazione», rendere ValidateOnStart() una regola per la configurazione delle app aziendali. 6

5. Dove collocare le impostazioni che l’app deve poter scrivere

appsettings.json è un posto per “valori predefiniti in sola lettura”; non è il posto per impostazioni che l’applicazione riscrive autonomamente. Molte app aziendali vengono installate in Program Files, dove gli utenti standard non hanno permessi di scrittura; ne deriva un’eccezione a runtime oppure la virtualizzazione del file system Windows fa divergere il contenuto visibile tra utenti.

Dividere le impostazioni scrivibili in base alla loro natura. È più sicuro ancorarle alle special folder ottenibili con Environment.GetFolderPath. 11

Posizione Come ottenerla Uso
%LOCALAPPDATA%\CompanyName\AppName Environment.SpecialFolder.LocalApplicationData Impostazione predefinita per dati e impostazioni per utente
%APPDATA%\CompanyName\AppName (Roaming) Environment.SpecialFolder.ApplicationData Solo per impostazioni che devono seguire l’utente in un ambiente di roaming profile
%ProgramData%\CompanyName\AppName Environment.SpecialFolder.CommonApplicationData Impostazioni per macchina condivise da tutti gli utenti; richiede progettazione degli ACL
using System;
using System.IO;
using System.Text.Json;

public sealed class UserSettingsStore
{
    private readonly string _filePath;

    public UserSettingsStore()
    {
        string root = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
        string dir = Path.Combine(root, "KomuraSoft", "MyApp");
        Directory.CreateDirectory(dir);
        _filePath = Path.Combine(dir, "user-settings.json");
    }

    public UserSettings Load()
    {
        if (!File.Exists(_filePath))
        {
            return new UserSettings();
        }
        string json = File.ReadAllText(_filePath);
        return JsonSerializer.Deserialize<UserSettings>(json) ?? new UserSettings();
    }

    public void Save(UserSettings settings)
    {
        string json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
        // Le scritture multiple nello stesso processo devono essere serializzate dal chiamante
        File.WriteAllText(_filePath, json);
    }
}

public sealed class UserSettings
{
    public string? LastOpenedFolder { get; set; }
    public int WindowWidth { get; set; } = 1024;
    public int WindowHeight { get; set; } = 768;
}

Mescolare impostazioni per utente e per macchina in un solo file provoca incidenti come “l’impostazione dell’utente A crea un problema all’utente B” nei computer condivisi. Per la separazione per utente/per macchina e la struttura dei profili Windows, vedere “Come funzionano i profili utente Windows”; per una tabella generale di scelta della posizione, “Come scegliere dove archiviare i dati di un’app Windows”.

6. Segreti: stringhe di connessione e API key

Scrivere direttamente in appsettings.json la password di una stringa di connessione o un’API key è un rischio concreto anche se il file è in .gitignore. Il testo in chiaro può fuoriuscire distribuendo il file in una cartella condivisa, facendoselo inviare durante una richiesta di supporto o lasciando in un backup un vecchio file di impostazioni dimenticato.

In sviluppo, usare Secret Manager (dotnet user-secrets). È però un meccanismo pensato per l’esperienza di sviluppo e non è cifrato. I valori sono salvati come JSON in chiaro in %APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json, e la documentazione ufficiale dichiara esplicitamente che non va trattato come trusted store ed è solo per lo sviluppo. Mettere segreti di produzione in user-secrets e distribuirli è un errore. 7

dotnet user-secrets init
dotnet user-secrets set "ExternalApi:ApiKey" "valore-di-sviluppo"

In produzione, usare DPAPI (Data Protection API) fornita da Windows, che cifra il valore legandolo alle credenziali dell’utente o della macchina. Protect/Unprotect di System.Security.Cryptography.ProtectedData sono metodi wrapper; DataProtectionScope.CurrentUser significa che il valore può essere decifrato solo sulla stessa macchina e con lo stesso account. Progettare ricordando che DPAPI è disponibile solo in Windows e su altre piattaforme genera PlatformNotSupportedException. 8

using System.Security.Cryptography;
using System.Text;

public static class SecretProtector
{
    // Aggiungere entropia che indichi lo scopo riduce il rischio di confonderla con dati protetti per altri scopi
    private static readonly byte[] Entropy = Encoding.UTF8.GetBytes("MyApp.ExternalApi.ApiKey");

    public static string Protect(string plainText)
    {
        byte[] plainBytes = Encoding.UTF8.GetBytes(plainText);
        byte[] protectedBytes = ProtectedData.Protect(plainBytes, Entropy, DataProtectionScope.CurrentUser);
        return Convert.ToBase64String(protectedBytes);
    }

    public static string Unprotect(string protectedBase64)
    {
        byte[] protectedBytes = Convert.FromBase64String(protectedBase64);
        byte[] plainBytes = ProtectedData.Unprotect(protectedBytes, Entropy, DataProtectionScope.CurrentUser);
        return Encoding.UTF8.GetString(plainBytes);
    }
}

I dettagli di implementazione — dove conservare il valore dopo la protezione DPAPI, quale scope scegliere tra CurrentUser e LocalMachine, e le insidie nelle app aziendali condivise da più utenti — sono approfonditi in “Conservare dati sensibili nelle app Windows: evitare impostazioni in chiaro con DPAPI”; consultarlo al momento dell’implementazione.

Il confine tra ciò che può restare in chiaro e ciò che non può è semplice. Valutare se la perdita del valore richiederebbe la rigenerazione di password o API key, l’indagine di accessi non autorizzati o una segnalazione a un’autorità di controllo. Il nome host o il numero di porta di una stringa di connessione di solito causano poco danno reale se divulgati; la password o il token di autenticazione inclusi sono invece sempre da proteggere. Costruendo una stringa di connessione separando “informazioni del server” e “credenziali” e applicando DPAPI solo alle seconde, si separa a livello di codice la parte relativamente innocua dalla parte da proteggere.

7. Modificare la configurazione a runtime: la realtà di reloadOnChange

La chiamata predefinita a AddJsonFile — quella eseguita internamente da Host.CreateApplicationBuilder — imposta reloadOnChange: true, quindi appsettings.json e appsettings.{Environment}.json vengono ricaricati automaticamente al rilevamento di una modifica. Internamente PhysicalFileProvider osserva le modifiche tramite FileSystemWatcher. 12

Il meccanismo presenta alcuni limiti pratici.

  • Si aggiornano solo i valori letti tramite IOptionsMonitor<T> (e IOptionsSnapshot<T>). IOptions<T> continua a mantenere il valore dell’avvio e resta quindi obsoleto anche dopo la riscrittura del file. La maggior parte dei casi «ho modificato direttamente il file ma non ha effetto» deriva dal confondere i due.
  • FileSystemWatcher non sempre consegna in modo affidabile le notifiche di modifica su file system come container Docker o condivisioni di rete. In questi ambienti impostare DOTNET_USE_POLLING_FILE_WATCHER a true passa al monitoraggio tramite polling a intervalli di 4 secondi; l’intervallo non è configurabile. 13 Per le particolarità generali di FileSystemWatcher — eventi persi, overflow del buffer e duplicati — vedere “Guida pratica a FileSystemWatcher”.
  • Una singola modifica del file può produrre più notifiche. Se l’app ripete un’operazione costosa a ogni notifica, occorre tenerlo presente: fare debounce di attivazioni ravvicinate o confrontare l’hash del contenuto e processare solo modifiche sostanziali.

Non è sempre necessario puntare a “applicare i cambi di configurazione senza riavvio”. Valori leggeri come livello di log o feature flag possono essere applicati subito con IOptionsMonitor, ma valori come la stringa di connessione al DB o la dimensione del thread pool, la cui modifica può confliggere immediatamente con risorse già in esecuzione, è più sicuro specificarli come “applicati al riavvio”. È più importante di reloadOnChange poter dire chiaramente al personale operativo “questa impostazione ha effetto al salvataggio” oppure “questa richiede un riavvio”.

8. Mappa di migrazione da app.config / Settings.settings

Quando si migra un’app dell’epoca .NET Framework a .NET, anche il meccanismo di configurazione va ricostruito. Ecco una corrispondenza tra i due. 14

.NET Framework .NET Note
<appSettings> in App.config / Web.config appsettings.json + IConfiguration La struttura gerarchica si esprime naturalmente con l’annidamento JSON
ConfigurationManager.AppSettings["Key"] builder.Configuration["Key"] o IOptions<T> Da accesso con chiavi stringa ad accesso tipizzato
ConfigurationManager.ConnectionStrings builder.Configuration.GetConnectionString("Name") La convenzione della sezione ConnectionStrings resta invariata
Settings.settings (user scope) File JSON personalizzato sotto %LOCALAPPDATA% Non esiste un meccanismo autogenerato come ApplicationSettingsBase; serializzazione e salvataggio sono propri
Settings.settings (application scope) appsettings.json Trattarlo come valori predefiniti in sola lettura
Cifratura di <connectionStrings> (aspnet_regiis ecc.) DPAPI (ProtectedData) Il meccanismo di cifratura cambia interamente e deve essere ricostruito nella migrazione

Durante la migrazione si incontrano spesso due trappole.

La prima è che l’aggiunta del pacchetto NuGet System.Configuration.ConfigurationManager permette al codice esistente che legge App.config di continuare a funzionare senza modifiche. È una scelta valida come primo stadio della migrazione, ma non bisogna fermarsi lì: pianificare lo spostamento a appsettings.json. Le librerie accessorie, come i logging provider, ormai assumono quasi universalmente appsettings.json, quindi mantenere il vecchio meccanismo solo per leggere App.config ha sempre meno ragioni. 14

La seconda riguarda le impostazioni user-scoped di Settings.settings. In .NET Framework una semplice chiamata a Properties.Settings.Default.Save() salvava automaticamente le impostazioni utente, ma .NET non fornisce un equivalente autogenerato pronto all’uso. Durante la migrazione preparare una propria storage class, come quella del capitolo 5.

La migrazione ha anche altri aspetti: compatibilità delle librerie dipendenti, presenza di COM interop, ripensamento della distribuzione e quadro complessivo dei controlli oltre la configurazione. Questa prospettiva di inventario è raccolta nella checklist pre-migrazione .NET Framework → .NET, che consigliamo di leggere all’inizio del progetto.

La gestione della configurazione .NET nasce dalla combinazione di tre meccanismi: stratificazione dei provider tramite IConfiguration, ricezione type-safe tramite la famiglia IOptions e cambio dell’ambiente tramite variabili d’ambiente. Fino a questo punto la maggior parte delle app può usare lo stesso metodo. Le preoccupazioni specifiche delle applicazioni aziendali Windows si concentrano oltre: dove mettere le impostazioni scrivibili, come proteggere i segreti, quanto consentire i cambi a runtime e come dismettere gli asset della generazione app.config.

Andare oltre “è tutto scritto in appsettings.json” e separare posizione e gestione per ogni tipo di impostazione. Ci auguriamo che la tabella decisionale dell’articolo sia un punto di partenza. L’inventario della configurazione di un’app esistente o la definizione di una strategia di migrazione da app.config spesso non portano alla risposta migliore senza vedere i file delle impostazioni e l’ambiente di distribuzione reali; in caso di dubbio, contattateci.

Articoli correlati

Aree di consulenza correlate

KomuraSoft LLC offre consulenza tecnica sulla progettazione della gestione della configurazione per applicazioni aziendali Windows, sulla progettazione operativa delle impostazioni per ambiente e sulla strategia di migrazione da asset app.config esistenti.

Riferimenti

  1. Microsoft Learn, Configuration in .NET - Alternative hosting approach. Sull’ordine di priorità dei provider assemblati per impostazione predefinita da Host.CreateApplicationBuilder: argomenti della riga di comando → variabili d’ambiente → user secrets in sviluppo → appsettings.{Environment}.json → appsettings.json.  2 3

  2. Microsoft Learn, .NET Generic Host - Host builder settings. Sull’ordine predefinito della host configuration (variabili con prefisso DOTNET_, argomenti) e della app configuration (appsettings.json, appsettings.{Environment}.json, Secret Manager, variabili d’ambiente, argomenti) caricate da Host.CreateApplicationBuilder 2

  3. Microsoft Learn, Use the .NET Generic Host in a Windows Forms app. Sui passaggi per integrare Generic Host in un’app Windows Forms e usare DI, configurazione e logging. 

  4. Microsoft Learn, ASP.NET Core runtime environments - Environment variables that determine the runtime environment. Sulla relazione tra DOTNET_ENVIRONMENT e ASPNETCORE_ENVIRONMENT, la priorità del primo con WebApplication, il predefinito Production e la differenza di sensibilità al maiuscolo tra Windows e Linux.  2

  5. Microsoft Learn, Options pattern in .NET - Options interfaces. Sulle differenze di lifetime, momento di riflessione delle modifiche e funzionalità supportate tra IOptions<TOptions>, IOptionsSnapshot<TOptions> e IOptionsMonitor<TOptions> 2

  6. Microsoft Learn, Options pattern in .NET - Options validation. Sulla configurazione della validazione DataAnnotations tramite ValidateDataAnnotations() e della validazione all’avvio tramite ValidateOnStart() o AddOptionsWithValidateOnStart 2 3

  7. Microsoft Learn, Safe storage of app secrets in development in ASP.NET Core - Use the Secret Manager tool. Sul fatto che Secret Manager conserva i segreti come JSON in chiaro in %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json, senza cifrarli, e che è solo per lo sviluppo.  2

  8. Microsoft Learn, ProtectedData Class. Su ProtectedData.Protect/Unprotect come wrapper di DPAPI, la differenza di scope CurrentUser/LocalMachine e il comportamento solo Windows con PlatformNotSupportedException su altre piattaforme.  2

  9. Microsoft Learn, Configuration providers in .NET. Sul meccanismo dei provider che unifica fonti differenti — JSON, variabili d’ambiente, riga di comando, INI, XML e altro — dietro IConfiguration

  10. Microsoft Learn, Configuration providers in .NET - Environment variable configuration provider. Su come la configurazione predefinita carica variabili DOTNET_ e argomenti nella host configuration, le normali variabili nella app configuration e come aggiungere un prefisso personalizzato. 

  11. Microsoft Learn, Environment.GetFolderPath Method. Sul recupero dei percorsi special folder con Environment.SpecialFolder e GetFolderPath

  12. Microsoft Learn, Detect changes with change tokens in ASP.NET Core - Monitor for configuration changes. Sul parametro reloadOnChange di AddJsonFile e sull’uso interno di PhysicalFileProvider e FileSystemWatcher per osservare il file delle impostazioni. 

  13. Microsoft Learn, Options pattern in .NET - IOptionsMonitor. Sulla limitazione delle notifiche IOptionsMonitor ai provider basati su file system e sul passaggio al polling ogni 4 secondi con DOTNET_USE_POLLING_FILE_WATCHER quando le notifiche non sono affidabili in container Docker o condivisioni di rete. 

  14. Microsoft Learn, Modernize after upgrading to .NET from .NET Framework - App.config. Sui passaggi di migrazione da App.config a appsettings.json, sulla compatibilità temporanea con il pacchetto System.Configuration.ConfigurationManager e sul pacchetto Microsoft.Extensions.Configuration.Json 2

Articoli recenti con gli stessi tag per approfondire argomenti vicini.

Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.

L’articolo è direttamente collegato ai servizi seguenti.

Domande frequenti

Domande che ricorrono nelle consulenze sull’argomento dell’articolo.

Va bene mettere appsettings.json nella stessa cartella dell'exe?
Sì, purché contenga valori predefiniti in sola lettura. Tuttavia, se quella cartella viene installata in Program Files, l'applicazione non può essere progettata per riscrivere appsettings.json. Gli utenti standard non hanno permessi di scrittura sotto Program Files: si avrà un'eccezione a runtime oppure la virtualizzazione del file system di Windows mostrerà silenziosamente contenuti diversi a utenti diversi. Separare ogni impostazione che deve essere modificabile in un file sotto %LOCALAPPDATA% o %ProgramData%.
Devono avere priorità le variabili d'ambiente o appsettings.json?
Non modificare la precedenza predefinita: mantenere l'ordine di override integrato appsettings.json → appsettings.{Environment}.json → variabili d'ambiente → argomenti della riga di comando. In caso di dubbio, valutare la natura del valore: i valori predefiniti che possono essere distribuiti nell'output della build appartengono ad appsettings.json, mentre quelli che variano per destinazione di distribuzione o contenitore appartengono alle variabili d'ambiente. Questa separazione mantiene chiare le responsabilità anche in seguito.
Come devo suddividere le classi delle impostazioni?
La regola di base è dividere le classi options per funzionalità o responsabilità — ConnectionOptions per le stringhe di connessione, ExternalApiOptions per l'integrazione con API esterne e così via — invece di riunire impostazioni non correlate in una sola classe. In questo modo si separano naturalmente anche le unità di validazione e ricaricamento per IOptionsSnapshot/IOptionsMonitor, e la validazione DataAnnotations resta autonoma per ciascuna classe.
Devo migrare da file INI o dal Registro?
Se si ricostruisce la gestione della configurazione da zero, consigliamo di standardizzare su appsettings.json + IConfiguration. .NET include anche un provider di configurazione INI, quindi è possibile continuare a leggere i file INI esistenti durante una migrazione graduale. Il Registro non rientra nell'ambito dei provider standard .NET; se esistono asset che ne dipendono, un approccio pratico è creare un bridge in sola lettura e spostare gradualmente i valori in appsettings.json.

Profilo dell’autore

Pagina di presentazione dell’autore dell’articolo.

Go Komura

Rappresentante di KomuraSoft LLC

Specializzato nello sviluppo di software Windows, nella consulenza tecnica e nell’analisi dei malfunzionamenti, soprattutto nei progetti con sistemi esistenti e guasti difficili da riprodurre.

Torna al blog