Configurazione, logging e Options pattern in .NET
Un'applicazione di produzione ha bisogno di due infrastrutture spesso trascurate ma essenziali: la configurazione, per adattare il comportamento senza ricompilare il codice, e il logging, per capire cosa accade durante l'esecuzione. La piattaforma .NET fornisce sistemi integrati per entrambe, costruiti attorno alla dependency injection studiata nella terza serie. In questo articolo vediamo come leggere la configurazione, come tipizzarla con l'Options pattern e come registrare messaggi di log in modo strutturato.
Il sistema di configurazione
La configurazione in .NET è un insieme di coppie chiave-valore raccolte da più sorgenti: file JSON, variabili d'ambiente, argomenti da riga di comando e altro. Le sorgenti vengono combinate secondo un ordine di priorità, così che, ad esempio, una variabile d'ambiente possa sovrascrivere un valore del file in produzione. La sorgente più comune è il file appsettings.json.
{
"ConnectionStrings": {
"Default": "Data Source=app.db"
},
"EmailSettings": {
"SmtpServer": "smtp.example.com",
"Port": 587,
"SenderName": "Notifiche"
}
}
Leggere i valori
Nelle applicazioni ASP.NET Core, l'interfaccia IConfiguration è disponibile tramite dependency injection e permette di accedere ai valori per chiave, usando i due punti per navigare la struttura annidata. È l'approccio più diretto, adatto a letture occasionali.
// Accesso diretto tramite chiave gerarchica
string? server = configuration["EmailSettings:SmtpServer"];
// Lettura tipizzata con conversione
int port = configuration.GetValue<int>("EmailSettings:Port");
// Metodo dedicato alle stringhe di connessione
string? connection = configuration.GetConnectionString("Default");
L'Options pattern
Accedere alla configurazione per stringhe è fragile: un errore di battitura nella chiave si scopre solo a runtime. L'Options pattern risolve il problema mappando una sezione della configurazione su una classe fortemente tipizzata. Si definisce una classe che rispecchia la struttura, e la si collega alla sezione corrispondente.
// La classe rispecchia la sezione EmailSettings
class EmailSettings
{
public string SmtpServer { get; set; } = "";
public int Port { get; set; }
public string SenderName { get; set; } = "";
}
// Registrazione nel punto di avvio: collega la sezione alla classe
builder.Services.Configure<EmailSettings>(
builder.Configuration.GetSection("EmailSettings"));
A questo punto la configurazione tipizzata si riceve tramite dependency injection usando l'interfaccia IOptions<T>, che espone i valori tramite la proprietà Value. Il vantaggio è duplice: sicurezza sui tipi e dipendenze esplicite.
using Microsoft.Extensions.Options;
class EmailService
{
private readonly EmailSettings _settings;
// La configurazione tipizzata e iniettata dal contenitore
public EmailService(IOptions<EmailSettings> options)
{
_settings = options.Value;
}
public void Send(string message)
{
Console.WriteLine($"Invio tramite {_settings.SmtpServer}:{_settings.Port}");
}
}
Configurazione per ambiente
Le applicazioni si comportano diversamente in sviluppo e in produzione. Il sistema di configurazione supporta file specifici per ambiente, come appsettings.Development.json, che sovrascrivono i valori del file base quando l'applicazione gira nell'ambiente corrispondente. L'ambiente è determinato da una variabile d'ambiente, permettendo di mantenere impostazioni distinte senza modificare il codice.
Il sistema di logging
Il logging registra ciò che accade durante l'esecuzione, dagli eventi ordinari agli errori. .NET fornisce l'astrazione ILogger<T>, iniettabile in qualsiasi classe, che invia i messaggi ai provider configurati: la console, un file, un servizio esterno. Il parametro di tipo indica la categoria del log, tipicamente la classe che lo emette.
using Microsoft.Extensions.Logging;
class OrderService
{
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger)
{
_logger = logger;
}
public void PlaceOrder(int orderId)
{
_logger.LogInformation("Elaborazione ordine {OrderId}", orderId);
try
{
// logica dell'ordine
}
catch (Exception ex)
{
_logger.LogError(ex, "Errore nell'ordine {OrderId}", orderId);
}
}
}
I livelli di log
Ogni messaggio ha un livello di severità che ne indica l'importanza. Configurando la soglia minima si controlla quali messaggi vengono effettivamente registrati, filtrando il rumore in produzione e aumentando il dettaglio in fase di diagnosi.
| Livello | Uso tipico |
|---|---|
| Trace | Dettaglio massimo, solo per diagnosi approfondita |
| Debug | Informazioni utili durante lo sviluppo |
| Information | Eventi ordinari del normale funzionamento |
| Warning | Situazioni anomale ma non bloccanti |
| Error | Errori che impediscono un'operazione |
| Critical | Guasti gravi che richiedono attenzione immediata |
Il logging strutturato
Un dettaglio importante degli esempi è l'uso di segnaposto nominati come {OrderId} anziché la concatenazione di stringhe. Questo abilita il logging strutturato: i valori vengono registrati come dati distinti, non solo come testo, permettendo ai sistemi di raccolta dei log di filtrare e cercare per quei valori. È una pratica da adottare sistematicamente.
// Corretto: i valori restano dati strutturati e interrogabili
_logger.LogInformation("Utente {UserId} ha effettuato {Count} ordini", userId, count);
// Da evitare: la concatenazione perde la struttura
_logger.LogInformation("Utente " + userId + " ha effettuato " + count + " ordini");
Conclusione
Abbiamo esplorato l'infrastruttura di configurazione e logging di .NET. Abbiamo letto i valori da appsettings.json tramite IConfiguration, tipizzato la configurazione con l'Options pattern e IOptions<T>, e gestito le differenze tra ambienti. Sul fronte del logging, abbiamo usato ILogger<T>, compreso i livelli di severità e adottato il logging strutturato con segnaposto nominati. Nel prossimo articolo torneremo ad ASP.NET Core per capire come le richieste vengono elaborate attraverso la pipeline dei middleware.