Site icon Ryadel

ASP.NET Core: Gestione dei parametri di configurazione

How to fix the "No executable found matching command dotnet-ef" error in Visual Studio with .NET Core

Quando si imposta la configurazione di un progetto ASP.NET Core, l’errore più comune è partire in modo semplice e ritrovarsi dopo pochi mesi con valori sparsi ovunque: un po’ dentro appsettings.json, un po’ nelle variabili d’ambiente, un po’ dentro Docker Compose, magari con qualche secret gestito a mano e connessioni duplicate in più punti.

Il problema non è solo estetico: una configurazione disordinata rende più difficile il deploy, aumenta il rischio di inconsistenze tra ambienti e complica sia il lavoro degli sviluppatori sia quello di chi deve gestire CI/CD, container e produzione.

Un approccio molto più solido, che io personalmente utilizzo in (quasi) tutti i miei progetti, consiste nel costruire la configurazione attorno alla pipeline standard di Microsoft.Extensions.Configuration, definendo una catena di provider chiara e prevedibile, dove ogni layer può sovrascrivere quello precedente.

Configuration chain

In uno scenario ben strutturato, la configurazione può essere composta in questo ordine:

La regola è semplice: i provider aggiunti dopo vincono su quelli caricati prima.

Questo schema funziona bene perché separa in modo naturale i diversi livelli di responsabilità:

  • appsettings.json contiene la struttura e gli eventuali valori di default non sensibili
  • .env contiene i valori standard per sviluppo locale o stack Docker
  • .env.local gestisce le differenze della macchina host quando si esegue l’app fuori dai container
  • le variabili di processo coprono shell, pipeline CI/CD e override runtime
  • Azure Key Vault gestisce i secret in produzione
  • un eventuale composer finale costruisce valori derivati solo quando mancano

È un modello che consiglio spesso perché resta leggibile anche quando il progetto cresce. Finché la gerarchia è chiara, capire “da dove arriva un valore” smette di essere un incubo.

appsettings.json

appsettings.json dovrebbe descrivere la forma della configurazione, non custodire i dati sensibili. In altre parole, va benissimo usarlo per definire sezioni, opzioni tipizzate e default innocui, mentre endpoint reali, password, chiavi API e host specifici dovrebbero arrivare da layer successivi.

Ad esempio:

Questo approccio mantiene il file versionabile, portabile e adatto a essere condiviso nel repository senza esporre dati che non dovrebbero stare lì.

Convenzioni JSON, environment variables e Azure Key Vault

Uno degli aspetti più importanti è adottare una convenzione di naming coerente tra tutti i provider.

Nel mondo ASP.NET Core, la chiave logica resta sempre la stessa, ma cambia il separatore a seconda del layer:

  • in appsettings.json si usa :
  • nelle variabili d’ambiente si usa __
  • in Azure Key Vault si usa normalmente --

Esempio:

Dal punto di vista dell’applicazione, tutte queste forme puntano alla stessa chiave logica.

Questo è uno dei punti più forti del sistema di configurazione di .NET: il codice applicativo non deve sapere se un valore arriva da JSON, da una variabile d’ambiente o da un secret remoto. Continua semplicemente a leggere Database:Host.

Doppio underscore o doppio trattino?

La scelta non è arbitraria:

  • __ è la convenzione usata dalle environment variables per rappresentare i separatori gerarchici
  • -- è la convenzione comunemente usata in Azure Key Vault per mappare la stessa gerarchia

Stabilire questa regola fin dall’inizio evita molte ambiguità, soprattutto nei team dove sviluppo, DevOps e operations lavorano sugli stessi parametri in contesti diversi.

Il ruolo dei file .env e .env.local

I file .env sono molto utili per standardizzare lo sviluppo locale, soprattutto quando si usano Docker e Docker Compose.

La prassi che utilizzo io è quella di creare due file distinti:

  • .env per i valori condivisi dello stack locale o containerizzato
  • .env.local per gli override specifici della macchina host

Il senso di questa separazione dipende dagli scenari di utilizzo concreto del codice propri della maggior parte dei development team: tipicamente capita di eseguire database, code o servizi ausiliari dentro Docker, ma l’applicazione direttamente da IDE o da dotnet run. In questi casi cambiano host, porte pubblicate e qualche endpoint locale. Mettere queste differenze in .env.local evita di sporcare il file principale con eccezioni personali.

Un esempio pratico potrebbe essere questo:

Il punto fondamentale è che questi file non dovrebbero introdurre una logica alternativa: dovrebbero semplicemente alimentare la normale pipeline di environment variables, così da mantenere un solo modello mentale.

Quando caricare il file .env.local

In genere ha senso caricare .env.local solo fuori dai container. Se l’app gira già dentro Docker, quel file locale non dovrebbe entrare in gioco.

Questa distinzione è importante perché impedisce che un override pensato per la macchina dello sviluppatore finisca per contaminare un ambiente containerizzato dove gli host corretti sono, per esempio, i nomi dei servizi Docker e non localhost.

Environment variables come override layer

Le variabili d’ambiente restano il layer più versatile dell’intera architettura. Funzionano bene in locale, in Docker, nelle pipeline CI/CD, nei servizi PaaS e in molti orchestratori.

In pratica conviene considerarle il canale standard per:

  • override temporanei
  • parametri di deploy
  • configurazioni per ambiente
  • segreti passati dal runtime o dalla piattaforma ospitante

Questo consente di evitare il proliferare di file specifici per ogni scenario. La logica resta una sola: stessa chiave logica, provider differente.

Valori composti e derivati

Un pattern molto utile, ma spesso sottovalutato, è quello dei valori composti. Invece di chiedere agli operatori di valorizzare sia i singoli componenti sia il valore finale aggregato, si possono calcolare alcune chiavi a startup partendo da elementi più semplici.

Il caso classico è la connection string:

che può essere costruita a partire da:

  • Database:Host
  • Database:Port
  • Database:Name
  • Database:Username
  • Database:Password

Lo stesso ragionamento si può applicare ad authority URL, endpoint completi o altre configurazioni derivate da componenti atomici.

La regola corretta, però, è una sola: comporre il valore soltanto se la chiave finale è ancora vuota. Se qualcuno ha già fornito il valore completo, bisogna rispettarlo.

Questo dettaglio fa la differenza tra una soluzione utile e una soluzione invasiva: il composer deve essere un fallback intelligente, non una sovrascrittura arbitraria.

Se applicato correttamente, l'approccio descritto porta i seguenti vantaggi:

  • riduce la duplicazione
  • evita incoerenze tra componenti e valore finale
  • semplifica il setup locale
  • lascia comunque aperta la possibilità di specificare il valore completo quando serve

Configurazione dell’ambiente locale

Quando il progetto usa Docker Compose, il file .env può diventare il punto centrale per alimentare sia l’interpolazione del compose file sia le variabili passate ai container applicativi.

Questo permette di gestire in un solo posto:

  • nome del progetto Compose
  • ambiente .NET
  • porte pubbliche
  • binding dell’applicazione
  • parametri dei servizi di supporto

Ad esempio:

Qui c’è anche una distinzione utile da fare. Non tutte le variabili presenti in Docker Compose devono necessariamente seguire il naming delle option class di ASP.NET Core. Alcune immagini richiedono nomi imposti dall’upstream, spesso in maiuscolo e con sintassi non negoziabile. In quei casi conviene accettare il vincolo, ma mantenere il resto della configurazione coerente.

Azure Key Vault per i secret di produzione

In produzione, spostare i secret in Azure Key Vault è una scelta naturale per molti progetti ASP.NET Core ospitati in Azure o integrati con l’ecosistema Microsoft.

L’ordine di registrazione conta molto: il provider di Key Vault dovrebbe essere aggiunto dopo le environment variables e prima dell’eventuale composizione dei valori derivati.

Un esempio tipico:

In questo modo i secret remoti possono sostituire i valori precedenti, ma i valori composti possono ancora usare ciò che risulta disponibile al termine della catena.

Le chiavi salvate nel vault dovrebbero seguire la convenzione con doppio trattino:

Il vantaggio vero è che il codice dell’applicazione non cambia. Cambia solo il provider che alimenta quelle stesse chiavi logiche.

Sezioni di configurazione

Una configurazione ben progettata non è solo una questione di provider, ma anche di struttura interna. In genere conviene suddividere i parametri in sezioni coerenti, ognuna dedicata a una responsabilità precisa.

Alcuni esempi comuni:

  • Database per host, porta, nome database e credenziali
  • Authentication o Identity per OIDC, OAuth o parametri di login federato
  • Storage per object storage e file service
  • Messaging per broker, code e sistemi asincroni
  • Cache per Redis o sistemi equivalenti
  • Email per SMTP o provider transazionali
  • Logging e Telemetry per osservabilità e diagnostica
  • Security per chiavi, cifratura, policy e impostazioni sensibili

Conviene evitare sezioni monolitiche o nomi troppo generici. Quando tutto finisce dentro un contenitore tipo Settings o App, il risultato è quasi sempre una perdita di chiarezza.

Errori comuni da evitare

A prescindere dal modello di configurazione che intendete adottare, è importante evitare alcuni errori molto comuni che, all’inizio, possono sembrare scorciatoie innocue ma che nel tempo tendono a trasformarsi in problemi operativi, incoerenze tra ambienti e difficoltà di manutenzione.

  • Mettere i secret in appsettings.json. È ancora diffusissimo. Funziona, certo, ma crea subito problemi di sicurezza, versionamento e portabilità.
  • Duplicare lo stesso dato in più formati. Se una connection string completa convive con host, porta, database, username e password, bisogna definire chiaramente chi comanda. Senza una regola precisa, prima o poi quei valori andranno fuori sync.
  • Mescolare configurazione applicativa e dettagli infrastrutturaliAlcuni parametri servono all’applicazione, altri al container, altri ancora all’immagine di terze parti. Vanno distinti, anche se convivono nello stesso ecosistema.
  • Usare localhost ovunque. localhost va bene sulla macchina host, ma quasi mai dentro un container. Se non si separano bene i layer locali da quelli containerizzati, i problemi arrivano subito.
  • Non rispettare l’ordine dei provider. La pipeline di configurazione di ASP.NET Core è semplice, ma solo finché l’ordine è intenzionale. Se si aggiunge un provider nel punto sbagliato, gli override smettono di comportarsi come previsto.

Conclusioni

Configurare bene un progetto ASP.NET Core non significa solo riempire i file appsettings di parametri, ma progettare una pipeline coerente, leggibile e sostenibile nel tempo. La combinazione tra appsettings.json, file .env, override locali, environment variables, secret esterni e valori derivati rappresenta un modello molto efficace perché separa i ruoli, riduce la duplicazione e mantiene il codice applicativo indipendente dalla fonte concreta dei dati.

È una soluzione che scala bene dal progetto piccolo fino a scenari più strutturati con Docker, CI/CD e ambienti cloud. E soprattutto evita una cosa che, in fase operativa, pesa molto più di quanto sembri: dover ricordare eccezioni, convenzioni implicite e scorciatoie accumulate nel tempo. Se la configurazione è pensata bene dall’inizio, anche il resto del progetto tende a invecchiare meglio.

Exit mobile version