Modello options in .NET
Il modello di opzioni usa classi per fornire l'accesso fortemente tipizzato ai gruppi di impostazioni correlate. Quando le impostazioni di configurazione vengono isolate in base allo scenario in classi separate, l'app aderisce a due importanti principi di progettazione del software:
- Principio di segregazione delle interfacce (Interface Segregation Principle, ISP) o incapsulamento: gli scenari (classi) che dipendono dalle impostazioni di configurazione dipendono solo dalle impostazioni di configurazione che usano.
- Separazione delle competenze (Separation of Concerns, SoC): le impostazioni di parti diverse dell'app non sono dipendenti o accoppiate l'una con l'altra.
Le opzioni offrono anche un meccanismo per convalidare i dati di configurazione. Per altre informazioni, vedere la sezione Opzioni di convalida.
Associare la configurazione gerarchica
Il modo preferito per leggere i valori di configurazione correlati prevede l'uso del modello di opzioni. Il modello options è possibile tramite l'interfaccia IOptions<TOptions>, in cui il parametro di tipo generico TOptions
è vincolato a un oggetto class
. L'interfaccia IOptions<TOptions>
può essere specificata in un secondo momento tramite l'inserimento delle dipendenze. Per altre informazioni, vedere Inserimento delle dipendenze in .NET.
Ad esempio, per leggere i valori di configurazione evidenziati da un file appsettings.json:
{
"SecretKey": "Secret key value",
"TransientFaultHandlingOptions": {
"Enabled": true,
"AutoRetryDelay": "00:00:07"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
Creare la classe TransientFaultHandlingOptions
seguente:
public sealed class TransientFaultHandlingOptions
{
public bool Enabled { get; set; }
public TimeSpan AutoRetryDelay { get; set; }
}
Quando si usa il modello options, una classe options:
- Deve essere non astratta con un costruttore pubblico senza parametri
- Deve contenere proprietà pubbliche di lettura/scrittura da associare (i campi sono non associati)
Il codice seguente fa parte del file C# Program.cs e:
- Chiama ConfigurationBinder.Bind per associare la classe
TransientFaultHandlingOptions
alla sezione"TransientFaultHandlingOptions"
. - Visualizza i dati di configurazione di .
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using ConsoleJson.Example;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Configuration.Sources.Clear();
IHostEnvironment env = builder.Environment;
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true);
TransientFaultHandlingOptions options = new();
builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
.Bind(options);
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
using IHost host = builder.Build();
// Application code should start here.
await host.RunAsync();
// <Output>
// Sample output:
Nel codice precedente la sezione "TransientFaultHandlingOptions"
del file di configurazione JSON è associata all'istanza di TransientFaultHandlingOptions
. In questo modo, le proprietà degli oggetti C# vengono attivate con i valori corrispondenti della configurazione.
ConfigurationBinder.Get<T>
associa e restituisce il tipo specificato. ConfigurationBinder.Get<T>
può risultare più utile rispetto all'uso di ConfigurationBinder.Bind
. Il codice seguente mostra come usare ConfigurationBinder.Get<T>
con la classe TransientFaultHandlingOptions
:
var options =
builder.Configuration.GetSection(nameof(TransientFaultHandlingOptions))
.Get<TransientFaultHandlingOptions>();
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
Nel codice precedente, si usa ConfigurationBinder.Get<T>
per acquisire un'istanza dell'oggetto TransientFaultHandlingOptions
con i relativi valori delle proprietà popolati dalla configurazione sottostante.
Importante
La classe ConfigurationBinder espone diverse API, ad esempio .Bind(object instance)
e .Get<T>()
che sono non vincolate a class
. Quando si usa una qualsiasi delle interfacce Options, è necessario rispettare i vincoli della classe options indicati in precedenza.
Un approccio alternativo quando si usa il modello options consiste nell'associare la sezione "TransientFaultHandlingOptions"
e aggiungerla al contenitore del servizio di inserimento di dipendenze. Nel codice seguente TransientFaultHandlingOptions
viene aggiunta al contenitore del servizio con Configure e associata alla configurazione:
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.Configure<TransientFaultHandlingOptions>(
builder.Configuration.GetSection(
key: nameof(TransientFaultHandlingOptions)));
builder
nell'esempio precedente è un'istanza di HostApplicationBuilder.
Suggerimento
Il parametro key
è il nome della sezione di configurazione da cercare. Non deve corrispondere al nome del tipo che la rappresenta. Ad esempio, potrebbe essere presente una sezione denominata "FaultHandling"
e rappresentarla dalla classe TransientFaultHandlingOptions
. In questo caso, verrebbe passato "FaultHandling"
alla funzione GetSection. L'operatore nameof
viene usato per praticità quando la sezione denominata corrisponde al tipo previsto.
Quando si usa il codice precedente, il codice seguente legge le opzioni di posizione:
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class ExampleService(IOptions<TransientFaultHandlingOptions> options)
{
private readonly TransientFaultHandlingOptions _options = options.Value;
public void DisplayValues()
{
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
}
}
Nel codice precedente le modifiche al file di configurazione JSON dopo l'avvio dell'app non vengono lette. Per leggere le modifiche dopo l'avvio dell'app, usare IOptionsSnapshot o IOptionsMonitor per monitorare le modifiche man mano che si verificano e reagire di conseguenza.
Interfacce per le opzioni
- non supporta:
- La lettura dei dati di configurazione dopo l'avvio dell'app.
- Opzioni denominate
- È registrata come Singleton e può essere inserita in qualsiasi durata del servizio.
- È utile negli scenari in cui le opzioni devono essere ricalcolate a ogni risoluzione di inserimento, in durate con ambito o temporanee. Per altre informazioni, vedere Usare IOptionsSnapshot per leggere i dati aggiornati.
- È registrata come Con ambito e non può quindi essere inserita in un servizio Singleton.
- Supporta le opzioni denominate
- Consente di recuperare le opzioni e gestire le notifiche delle opzioni per le istanze di
TOptions
. - È registrata come Singleton e può essere inserita in qualsiasi durata del servizio.
- Supporta:
- Notifiche relative a modifiche
- Opzioni denominate
- Configurazione ricaricabile
- Invalidamento selettivo di opzioni (IOptionsMonitorCache<TOptions>)
IOptionsFactory<TOptions> è responsabile della creazione di nuove istanze di opzioni. Include un singolo metodo Create. L'implementazione predefinita accetta tutte le interfacce IConfigureOptions<TOptions> e IPostConfigureOptions<TOptions> registrate ed esegue tutte le configurazioni, seguite dalla post-configurazione. Fa distinzione tra IConfigureNamedOptions<TOptions> e IConfigureOptions<TOptions> e chiama solo l'interfaccia appropriata.
IOptionsMonitorCache<TOptions> viene usata da IOptionsMonitor<TOptions> per memorizzare nella cache le istanze di TOptions
. IOptionsMonitorCache<TOptions> invalida le istanze delle opzioni nel monitoraggio in modo che il valore venga ricalcolato (TryRemove). I valori possono essere introdotti manualmente con TryAdd. Il metodo Clear viene usato quando tutte le istanze denominate devono essere ricreate su richiesta.
IOptionsChangeTokenSource<TOptions> consente di recuperare l'oggetto IChangeToken che tiene traccia delle modifiche apportate all'istanza di TOptions
sottostante. Per altre informazioni sulle primitive dei token di modifica, vedere Modificare le notifiche.
Vantaggi delle interfacce options
L'uso di un tipo wrapper generico consente di separare la durata dell'opzione dal contenitore di inserimento delle dipendenze (DI). L'interfaccia IOptions<TOptions>.Value fornisce un livello di astrazione, inclusi i vincoli generici, sul tipo di opzioni. Fornisce i seguenti vantaggi:
- La valutazione dell'istanza di configurazione
T
viene posticipata all'accesso di IOptions<TOptions>.Value, anziché quando viene inserita. Questo aspetto è importante perché è possibile utilizzare l'opzioneT
da varie posizioni e scegliere la semantica di durata senza modificare nulla in merito aT
. - Quando si registrano opzioni di tipo
T
, non è necessario registrare in modo esplicito il tipoT
. Questo è molto utile quando si crea una libreria con impostazioni predefinite semplici e non si vuole forzare il chiamante a registrare le opzioni nel contenitore di inserimento delle dipendenze con una durata specifica. - Dal punto di vista dell'API, consente vincoli sul tipo
T
(in questo caso,T
è vincolato a un tipo riferimento).
Usare IOptionsSnapshot per leggere i dati aggiornati
Quando si usa IOptionsSnapshot<TOptions>, le opzioni vengono calcolate una volta per richiesta quando viene eseguito l'accesso e la memorizzazione nella cache per la durata della richiesta. Le modifiche alla configurazione vengono lette dopo l'avvio dell'app quando si usano provider di configurazione che supportano la lettura dei valori di configurazione aggiornati.
Differenze tra IOptionsMonitor
e IOptionsSnapshot
:
IOptionsMonitor
è un servizio singleton che recupera i valori delle opzioni correnti in qualsiasi momento, particolarmente utile nelle dipendenze singleton.IOptionsSnapshot
è un servizio con ambito e fornisce uno snapshot delle opzioni al momento della costruzione dell'oggettoIOptionsSnapshot<T>
. Gli snapshot delle opzioni sono progettati per l'uso con dipendenze temporanee e con ambito.
Il codice seguente usa IOptionsSnapshot<TOptions>.
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class ScopedService(IOptionsSnapshot<TransientFaultHandlingOptions> options)
{
private readonly TransientFaultHandlingOptions _options = options.Value;
public void DisplayValues()
{
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={_options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={_options.AutoRetryDelay}");
}
}
Il codice seguente registra un'istanza di configurazione che TransientFaultHandlingOptions
associa a:
builder.Services
.Configure<TransientFaultHandlingOptions>(
configurationRoot.GetSection(
nameof(TransientFaultHandlingOptions)));
Nel codice precedente, il metodo Configure<TOptions>
viene usato per registrare un'istanza di configurazione per l'associazione di TOptions
e aggiorna le opzioni quando la configurazione cambia.
IOptionsMonitor
Il tipo IOptionsMonitor
supporta le notifiche di modifica e abilita scenari in cui l'app potrebbe dover rispondere alle modifiche all'origine della configurazione in modo dinamico. Questo è utile quando è necessario reagire alle modifiche apportate ai dati di configurazione dopo l'avvio dell'app. Le notifiche di modifica sono supportate solo per i provider di configurazione basati su file system, ad esempio:
- Microsoft.Extensions.Configuration.Ini
- Microsoft.Extensions.Configuration.Json
- Microsoft.Extensions.Configuration.KeyPerFile
- Microsoft.Extensions.Configuration.UserSecrets
- Microsoft.Extensions.Configuration.Xml
Per usare il monitoraggio delle opzioni, gli oggetti options vengono configurati nello stesso modo in una sezione di configurazione.
builder.Services
.Configure<TransientFaultHandlingOptions>(
configurationRoot.GetSection(
nameof(TransientFaultHandlingOptions)));
Nell'esempio seguente viene usato IOptionsMonitor<TOptions>:
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class MonitorService(IOptionsMonitor<TransientFaultHandlingOptions> monitor)
{
public void DisplayValues()
{
TransientFaultHandlingOptions options = monitor.CurrentValue;
Console.WriteLine($"TransientFaultHandlingOptions.Enabled={options.Enabled}");
Console.WriteLine($"TransientFaultHandlingOptions.AutoRetryDelay={options.AutoRetryDelay}");
}
}
Nel codice precedente le modifiche al file di configurazione JSON dopo l'avvio dell'app vengono lette.
Suggerimento
È possibile che alcuni file system, ad esempio i contenitori Docker e le condivisioni di rete, non inviino sempre le notifiche di modifica. Quando si usa l'interfaccia IOptionsMonitor<TOptions> in questi ambienti, impostare la variabile di ambiente DOTNET_USE_POLLING_FILE_WATCHER
su 1
o true
per eseguire il polling del file system per le modifiche. L'intervallo di polling per le modifiche è ogni quattro secondi e non è configurabile.
Per altre informazioni sui contenitori Docker, vedere Distribuire un'app .NET in contenitori.
Supporto delle opzioni denominate con IConfigureNamedOptions
Le opzioni denominate:
- Sono utili quando più sezioni di configurazione sono associate alle stesse proprietà.
- Fanno distinzione tra maiuscole e minuscole.
Considerare il file appsettings.json seguente:
{
"Features": {
"Personalize": {
"Enabled": true,
"ApiKey": "aGEgaGEgeW91IHRob3VnaHQgdGhhdCB3YXMgcmVhbGx5IHNvbWV0aGluZw=="
},
"WeatherStation": {
"Enabled": true,
"ApiKey": "QXJlIHlvdSBhdHRlbXB0aW5nIHRvIGhhY2sgdXM/"
}
}
}
Anziché creare due classi per associare Features:Personalize
e Features:WeatherStation
, viene usata la classe seguente per ogni sezione:
public class Features
{
public const string Personalize = nameof(Personalize);
public const string WeatherStation = nameof(WeatherStation);
public bool Enabled { get; set; }
public string ApiKey { get; set; }
}
Nel codice seguente vengono configurate le opzioni denominate:
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Omitted for brevity...
builder.Services.Configure<Features>(
Features.Personalize,
builder.Configuration.GetSection("Features:Personalize"));
builder.Services.Configure<Features>(
Features.WeatherStation,
builder.Configuration.GetSection("Features:WeatherStation"));
Il codice seguente mostra le opzioni denominate:
public sealed class Service
{
private readonly Features _personalizeFeature;
private readonly Features _weatherStationFeature;
public Service(IOptionsSnapshot<Features> namedOptionsAccessor)
{
_personalizeFeature = namedOptionsAccessor.Get(Features.Personalize);
_weatherStationFeature = namedOptionsAccessor.Get(Features.WeatherStation);
}
}
Tutte le opzioni sono istanze denominate. Le istanze di IConfigureOptions<TOptions> sono considerate come destinate all'istanza di Options.DefaultName
, ovvero string.Empty
. IConfigureNamedOptions<TOptions> implementa anche IConfigureOptions<TOptions>. L'implementazione predefinita di IOptionsFactory<TOptions> include la logica per usarle in modo appropriato. L'opzione denominata null
viene usata per avere come destinazione tutte le istanze denominate anziché un'istanza denominata specifica. Questa convenzione è usata da ConfigureAll e PostConfigureAll.
API OptionsBuilder
OptionsBuilder<TOptions> viene usata per configurare le istanze di TOptions
. OptionsBuilder
semplifica la creazione di opzioni denominate perché è costituita da un singolo parametro nella chiamata iniziale ad AddOptions<TOptions>(string optionsName)
invece di essere visualizzata in tutte le chiamate successive. La convalida delle opzioni e gli overload ConfigureOptions
che accettano le dipendenze dei servizi sono disponibili solo tramite OptionsBuilder
.
OptionsBuilder
è usata nella sezione Convalida delle opzioni.
Usare i servizi di inserimento delle dipendenze per configurare le opzioni
Quando si configurano le opzioni, è possibile usare l'inserimento delle dipendenze per accedere ai servizi registrati e usarli per configurare le opzioni. Ciò è utile quando è necessario accedere ai servizi per configurare le opzioni. È possibile accedere ai servizi dall'inserimento delle dipendenze durante la configurazione delle opzioni in due modi:
Passare un delegato di configurazione in Configure in OptionsBuilder<TOptions >>.
OptionsBuilder<TOptions>
fornisce overload di Configure che consentono di usare fino a cinque servizi per configurare le opzioni:builder.Services .AddOptions<MyOptions>("optionalName") .Configure<ExampleService, ScopedService, MonitorService>( (options, es, ss, ms) => options.Property = DoSomethingWith(es, ss, ms));
Creare un tipo che implementa IConfigureOptions<TOptions> o IConfigureNamedOptions<TOptions> e registrare il tipo come servizio.
È consigliabile passare un delegato di configurazione a Configure poiché la creazione di un servizio è più complessa. La creazione di un tipo equivale alle operazioni che il framework esegue quando chiama Configure. La chiamata a Configure registra un'interfaccia IConfigureNamedOptions<TOptions> generica temporanea, che ha un costruttore che accetta i tipi di servizi generici specificati.
Convalida delle opzioni
La convalida delle opzioni consente di convalidare i valori delle opzioni.
Considerare il file appsettings.json seguente:
{
"MyCustomSettingsSection": {
"SiteTitle": "Amazing docs from Awesome people!",
"Scale": 10,
"VerbosityLevel": 32
}
}
La classe seguente viene associata alla sezione di configurazione "MyCustomSettingsSection"
e applica un paio di regole DataAnnotations
:
using System.ComponentModel.DataAnnotations;
namespace ConsoleJson.Example;
public sealed class SettingsOptions
{
public const string ConfigurationSectionName = "MyCustomSettingsSection";
[Required]
[RegularExpression(@"^[a-zA-Z''-'\s]{1,40}$")]
public required string SiteTitle { get; set; }
[Required]
[Range(0, 1_000,
ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public required int Scale { get; set; }
[Required]
public required int VerbosityLevel { get; set; }
}
Nella classe SettingsOptions
precedente, la proprietà ConfigurationSectionName
contiene il nome della sezione di configurazione a cui eseguire l'associazione. In questo scenario, l'oggetto opzioni fornisce il nome della relativa sezione di configurazione.
Suggerimento
Il nome della sezione di configurazione è indipendente dall'oggetto di configurazione a cui viene associata. In altre parole, una sezione di configurazione denominata "FooBarOptions"
può essere associata a un oggetto options denominato ZedOptions
. Sebbene assegnare lo stesso nome sia una prassi comune, non è necessario e possono effettivamente verificarsi conflitti tra nomi.
Il codice seguente:
- Chiama AddOptions per ottenere un oggetto OptionsBuilder<TOptions> che viene associato alla classe
SettingsOptions
. - Chiama ValidateDataAnnotations per abilitare la convalida tramite
DataAnnotations
.
builder.Services
.AddOptions<SettingsOptions>()
.Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
.ValidateDataAnnotations();
Il metodo di estensione ValidateDataAnnotations
viene definito nel pacchetto NuGet Microsoft.Extensions.Options.DataAnnotations.
Il codice seguente visualizza i valori di configurazione o segnala gli errori di convalida:
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
public sealed class ValidationService
{
private readonly ILogger<ValidationService> _logger;
private readonly IOptions<SettingsOptions> _config;
public ValidationService(
ILogger<ValidationService> logger,
IOptions<SettingsOptions> config)
{
_config = config;
_logger = logger;
try
{
SettingsOptions options = _config.Value;
}
catch (OptionsValidationException ex)
{
foreach (string failure in ex.Failures)
{
_logger.LogError("Validation error: {FailureMessage}", failure);
}
}
}
}
Il codice seguente applica una regola di convalida più complessa usando un delegato:
builder.Services
.AddOptions<SettingsOptions>()
.Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Scale != 0)
{
return config.VerbosityLevel > config.Scale;
}
return true;
}, "VerbosityLevel must be > than Scale.");
La convalida viene eseguita in fase di esecuzione, ma è possibile configurarla in modo che venga eseguita all'avvio concatenando una chiamata a ValidateOnStart
:
builder.Services
.AddOptions<SettingsOptions>()
.Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Scale != 0)
{
return config.VerbosityLevel > config.Scale;
}
return true;
}, "VerbosityLevel must be > than Scale.")
.ValidateOnStart();
A partire da .NET 8, è possibile usare un'API alternativa, AddOptionsWithValidateOnStart<TOptions>(IServiceCollection, String), che abilita la convalida all'avvio per un tipo options specifico:
builder.Services
.AddOptionsWithValidateOnStart<SettingsOptions>()
.Bind(Configuration.GetSection(SettingsOptions.ConfigurationSectionName))
.ValidateDataAnnotations()
.Validate(config =>
{
if (config.Scale != 0)
{
return config.VerbosityLevel > config.Scale;
}
return true;
}, "VerbosityLevel must be > than Scale.");
IValidateOptions
per la convalida complessa
La classe seguente implementa IValidateOptions<TOptions>:
using System.Text;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
sealed partial class ValidateSettingsOptions(
IConfiguration config)
: IValidateOptions<SettingsOptions>
{
public SettingsOptions? Settings { get; private set; } =
config.GetSection(SettingsOptions.ConfigurationSectionName)
.Get<SettingsOptions>();
public ValidateOptionsResult Validate(string? name, SettingsOptions options)
{
StringBuilder? failure = null;
if (!ValidationRegex().IsMatch(options.SiteTitle))
{
(failure ??= new()).AppendLine($"{options.SiteTitle} doesn't match RegEx");
}
if (options.Scale is < 0 or > 1_000)
{
(failure ??= new()).AppendLine($"{options.Scale} isn't within Range 0 - 1000");
}
if (Settings is { Scale: 0 } && Settings.VerbosityLevel <= Settings.Scale)
{
(failure ??= new()).AppendLine("VerbosityLevel must be > than Scale.");
}
return failure is not null
? ValidateOptionsResult.Fail(failure.ToString())
: ValidateOptionsResult.Success;
}
[GeneratedRegex("^[a-zA-Z''-'\\s]{1,40}$")]
private static partial Regex ValidationRegex();
}
IValidateOptions
consente lo spostamento del codice di convalida in una classe.
Nota
Questo codice di esempio si basa sul pacchetto NuGet Microsoft.Extensions.Configuration.Json.
Usando il codice precedente, la convalida viene abilitata quando si configurano i servizi con il codice seguente:
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
// Omitted for brevity...
builder.Services.Configure<SettingsOptions>(
builder.Configuration.GetSection(
SettingsOptions.ConfigurationSectionName));
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton
<IValidateOptions<SettingsOptions>, ValidateSettingsOptions>());
Post-configurazione delle opzioni
Impostare la post-configurazione con IPostConfigureOptions<TOptions>. La post-configurazione viene eseguita dopo tutte le operazioni di configurazione IConfigureOptions<TOptions> e può essere utile negli scenari in cui è necessario eseguire l'override della configurazione:
builder.Services.PostConfigure<CustomOptions>(customOptions =>
{
customOptions.Option1 = "post_configured_option1_value";
});
PostConfigure è disponibile per la post-configurazione delle opzioni denominate:
builder.Services.PostConfigure<CustomOptions>("named_options_1", customOptions =>
{
customOptions.Option1 = "post_configured_option1_value";
});
Usare PostConfigureAll per la post-configurazione di tutte le istanze di configurazione:
builder.Services.PostConfigureAll<CustomOptions>(customOptions =>
{
customOptions.Option1 = "post_configured_option1_value";
});