Pokusím se zde na malých ukázkách kódu postupně ukázat použití IConfiguration a IOptions v .NET Core.
Budu předpokládat standardní konfiguraci .NET Core aplikaci pomoci souboru appsettings.json načtenou například pomoci výchozího Host.CreateDefaultBuilder.
Mějme v něm přidanou tuto jednoduchou konfigurační sekci:
"AppSettings": {
"Settings1": "Value1",
"Settings2": "Value2"
},
Začneme základním načtení konfigurace z IConfiguration objektu.
IConfiguration
K hodnotám uvedené konfigurační sekce můžeme přistupovat napřímo pomoci objektu typu IConfiguration takto (kde objekt configuration získáme například pomoci DI přes konstruktor):
string settings1 = configuration["AppSettings:Settings1"]
Lepší způsob ale je si na konfigurační sekci zavést vlastní typový objekt:
[System.Diagnostics.DebuggerDisplay("\\{ Settings1 = {Settings1}, Settings2 = {Settings2} \\}")]
public sealed class AppSettingsOptions
{
public string Settings1 { get; set; }
public string Settings2 { get; set; }
}
a configuraci pak můžeme do něj načítat takto:
var appSettings = configuration.GetSection("AppSettings").Get<AppSettingsOptions>();
nebo do již existující instance takto:
var appSettings = new AppSettingsOptions();
configuration.GetSection("AppSettings").Bind(appSettings);
Pokud konfigurační soubor appsettings.json změníme, bude (díky reloadOnChange: true uvedeného při standardní konfiguraci) znovu načten. Nové volání načtení konfigurace pak bude změny obsahovat.
IOptions
Options pattern použijeme tak, že si nejprve (typicky ve startupu aplikace v metodě ConfigureServices) zaregistrujeme náš objekt AppSettingsOptions do containeru ServiceCollection. Provedeme to pomoci extenze Configure<T>(IConfiguration config) s předáním konfigurační sekce:
services.Configure<AppSettingsOptions>(this.configuration.GetSection("AppSettings"));
Při používání si pak pomoci DI v konstruktoru získáme naplněnou instanci objektu AppSettingsOptions pomoci IOptions interfaců. Máme tyto možnosti:
public class Test
{
private AppSettingsOptions AppSettingsOptions;
public Test(IOptions<AppSettingsOptions> appSettingsOptionsAccessor)
{
this.AppSettingsOptions = appSettingsOptionsAccessor.Value;
}
public Test(IOptionsSnapshot<AppSettingsOptions> appSettingsOptionsAccessor)
{
this.AppSettingsOptions = appSettingsOptionsAccessor.Value;
}
public Test(IOptionsMonitor<AppSettingsOptions> appSettingsOptionsAccessor)
{
this.AppSettingsOptions = appSettingsOptionsAccessor.CurrentValue;
var disposable = appSettingsOptionsAccessor.OnChange(options =>
{
this.AppSettingsOptions = options;
});
}
}
Při použití IOptions<T> bude přes vlastnost Value objekt typu AppSettingsOptions načten a nakešován jako Singleton. Při změně konfigurace v souboru appsettings.json nebudou nové hodnoty do této instance načteny.
Při použití IOptionsSnapshot<T> má získaný objekt typu AppSettingsOptions lifetime Scoped, tedy typicky request. Při novém requestu bude obsahovat změny konfigurace provedené v appsettings.json. Objekt se totiž vytváří při každém requestu znovu (a je po jeho dobu existence nakešován), to ale nemusí být někdy vhodné (například pokud máme v objektu nějakou složitější logiku).
IOptionsSnapshot ještě navíc podporují tzv. named options, když potřebujete mít více instancí konfigurace (registrují se uvedením jména při services.Configure<T>(string name, IConfiguration config).
Třetí možností je použití IOptionsMonitor<T>. Obsahuje vlastnost CurrentValue pro načtení objektu AppSettingsOptions, který bude Singleton (jako v případě IOptions<T>.Value). Navíc ale umí reagovat na změny konfigurace, tj. po změně bude CurrentValue vracet novou instanci se změněnými hodnotami. Přitom se ale nová instance vytváří pouze když je provedena změna (na rozdíl od IOptionsSnapshot<T>). Pokud sami potřebujeme reagovat na změnu konfigurace, máme možnost tak provést pomoci akce v OnChange viz. uvedený příklad. (OnChange vrací IDisposable, kde volání Dispose() ukončí přijímaní dalších změn a uvolní ChangeToken.). Named options jsou zde podporovány také.
Pokud jste to do teď neřešili a máte v kódu pouze IOptions, za mě doporučuji změnit a používat IOptionsMonitor.
IOptions with a delegate registration
Někdy může být potřeba options objekt z konfigurace vytvářet nějakou složitější logikou, bohužel jsem nenašel způsob jak do Configure předat nějakou vlastní faktory akci. Můžeme ale využít registraci do ServiceCollection pomoci delegátu, který vám předá vytvořenou instanci options objektu a umožní ho pak naplnit. Registraci provedeme extenzí Configure<T>(Action<TOptions> configureOptions), v kódu to může vypadat nějak takto:
var section = configuration.GetSection("AppOptions");
services.Configure<AppOptions>(options =>
{
section.Bind<AppOptions>(options);
//Fill (change) Setting2 with custom logic
options.Setting2 = GetSetting2(section);
});
//Register ChangeToken to respond to configuration changes (normaly registered in services.Configure(config) extension)
services.AddSingleton((IOptionsChangeTokenSource<IPAddressRulesOptions>)new ConfigurationChangeTokenSource<AppOptions>(section));
V příkladu naplním instanci našeho objektu AppSettingsOptions pomoci Bind, a pak změním vlastnosti Settings2 vlastní logikou (kterou zde představuje volání GetSettings2).
Je zde takový side effect, že takto zaregistrovaný options objekt nereaguje přes IOptionsMotitor<AppSettingsOptions> na změny konfigurace. To spravíme tím, že ještě zaregistrujeme IOptionsChangeTokenSource<T> resp. ConfigurationChangeTokenSource<T> který zajistí reagování na změny konfigurace. Normálně je tato registrace součástí extenze s parametrem config Configure(config), ale zde ve variantě Configure(configureOptions) chybí.
Použité reference:
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-3.1
https://andrewlock.net/creating-singleton-named-options-with-ioptionsmonitor/
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/change-tokens?view=aspnetcore-3.1