- Modern Web Development with ASP.NET Core 3
- Ricardo Peres
- 2284字
- 2021-06-18 18:35:56
Using configuration values
So, we've now seen how to set up configuration providers, but how exactly can we use these configuration values? Let's see in the following sections.
Getting and setting values explicitly
Remember that the .NET configuration allows you to set both reading and writing, both using the [] notation, as illustrated in the following code snippet:
var value = cfg["key"]; cfg["another.key"] = "another value";
Of course, setting a value in the configuration object does not mean that it will get persisted into any provider; the configuration is kept in memory only.
It is also possible to try to have the value converted to a specific type, as follows:
cfg["count"] = "0"; var count = cfg.GetValue<int>("count");
Configuration sections
It is also possible to use configuration sections. A configuration section is specified through a colon (:), as in section:subsection. An infinite nesting of sections can be specified. But—I hear you ask—what is a configuration section, and how do we define one? Well, that depends on the configuration source you're using.
In the case of JSON, a configuration section will basically map to a complex property. Have a look at the following code snippet to view an example of this:
{ "section-1": { "section-2": { "section-3": { "a-key": "value" } } } }
We have a couple of sections here, as follows:
- The root section
- section-1
- section-2
- section-3
So, if we wanted to access a value for the a-key key, we would do so using the following syntax:
var aKey = cfg["section-1:section-2:section-3:a-key"];
Alternatively, we could ask for the section-3 section and get the a-key value directly from it, as illustrated in the following code snippet:
var section3 = cfg.GetSection("section-1:section-2:section-3"); var aKey = section3["a-key"]; var key = section3.Key; //section-3 var path = section3.Path; //section-1:section-2:section-3
A section will contain the path from where it was obtained. This is defined in the IConfigurationSection interface, which inherits from IConfiguration, thus making all of its extension methods available too.
By the way, you can ask for any configuration section and a value will always be returned, but this doesn't mean that it exists. You can use the Exists extension method to check for that possibility, as follows:
var fairyLandSection = cfg.GetSection("fairy:land"); var exists = fairyLandSection.Exists(); //false
A configuration section may have children, and we can list them using GetChildren, like this:
var section1 = cfg.GetSection("section-1"); var subSections = section1.GetChildren(); //section-2
.NET Core includes a shorthand for a typical configuration section and connection strings. This is the GetConnectionString extension method, and it basically looks for a connection string named ConnectionStrings and returns a named value from it. You can use the JSON schema introduced when we discussed the JSON provider as a reference, as follows:
var blogConnectionString = cfg.GetConnectionString("DefaultConnection");
Getting all values
It may not be that useful, but it is possible to get a list of all configuration values (together with their keys) present in a configuration object. We do this using the AsEnumerable extension method, illustrated in the following code snippet:
var keysAndValues = cfg.AsEnumerable().ToDictionary(kv => kv.Key, kv => kv.Value);
There's also a makePathsRelative parameter, which, by default, is false and can be used in a configuration section to strip out the section's key from the returned entries' keys. Say, for example, that you are working on the section-3 section. If you call AsEnumerable with makePathsRelative set to true, then the entry for a-key will appear as a-key instead of section-1:section-2:section-3:a-key.
Binding to classes
Another interesting option is to bind the current configuration to a class. The binding process will pick up any sections and their properties present in the configuration and try to map them to a .NET class. Let's say we have the following JSON configuration:
{ "Logging": { "IncludeScopes": false, "LogLevel": { "Default": "Debug", "System": "Information", "Microsoft": "Information" } } }
We also have a couple of classes, such as these ones:
public class LoggingSettings { public bool IncludeScopes { get; set; } public LogLevelSettings LogLevel { get; set; } }
public class LogLevelSettings { public LogLevel Default { get; set; } public LogLevel System { get; set; } public LogLevel Microsoft { get; set; } }
You can bind the two together, like this:
var settings = new LoggingSettings { LogLevel = new LogLevelSettings() }; cfg.GetSection("Logging").Bind(settings);
The values of LoggingSettings will be automatically populated from the current configuration, leaving untouched any properties of the target instance for which there are no values in the configuration. Of course, this can be done for any configuration section, so if your settings are not stored at the root level, it will still work.
Mind you, these won't be automatically refreshed whenever the underlying data changes. We will see in a moment how we can do that.
Another option is to have the configuration build and return a self-instantiated instance, as follows:
var settings = cfg.GetSection("Logging").Get<LoggingSettings>();
For this to work, the template class cannot be abstract and needs to have a public parameterless constructor defined.
Since when using a file-based configuration, all properties are stored as strings, the providers need to know how to convert these into the target types. Fortunately, the included providers know how to do this for most types, such as the following:
- Strings
- Integers
- Floating points (provided the decimal character is the same as per the current culture)
- Booleans (true or false in any casing)
- Dates (the format must match the current culture or be compliant Request for Comments (RFC) 3339/International Organization for Standardization (ISO) 8601)
- Time (hh:mm:ss or RFC 3339/ISO 8601)
- GUIDs
- Enumerations
Injecting values
OK—so, we now know how to load configuration values from several sources, and we also know a couple of ways to ask for them explicitly. However, .NET Core relies heavily on dependency injection, so we might want to use that for configuration settings as well.
First, it should be fairly obvious that we can register the configuration object itself with the dependency injection framework, as follows:
var cfg = builder.Build(); services.AddSingleton(cfg);
Wherever we ask for an IConfigurationRoot object, we will get this one. We can also register it as the base IConfiguration, which is safe as well, although we miss the ability to reload the configuration (we will cover this in more detail later on). This is illustrated here:
services.AddSingleton<IConfiguration>(cfg);
We might also be interested in injecting a POCO class with configuration settings. In that case, we use Configure, as follows:
services.Configure<LoggingSettings>(settings => { settings.IncludeScopes = true; settings.Default = LogLevel.Debug; });
Here, we are using the Configure extension method, which allows us to specify values for a POCO class to be created at runtime whenever it is requested. Rather than doing this manually, we can ask the configuration object to do it, as follows:
services.Configure<LoggingSettings>(settings => { cfg.GetSection("Logging").Bind(settings); });
Even better, we can pass named configuration options, as follows:
services.Configure<LoggingSettings>("Elasticsearch", settings => { this.Configuration.GetSection("Logging:Elasticsearch").Bind(settings); });
services.Configure<LoggingSettings>("Console", settings =>
{
this.Configuration.GetSection("Logging:Console").Bind(settings);
});
In a minute, we will see how we can use these named configuration options.
We can even pass in the configuration root itself, or a sub-section of it, which is way simpler, as illustrated in the following code snippet:
services.Configure<LoggingSettings>(cfg.GetSection("Logging"));
Of course, we might as well register our POCO class with the dependency injection framework, as follows:
var cfg = builder.Build(); var settings = builder.GetSection("Logging").Get<LoggingSettings>(); services.AddSingleton(settings);
If we use the Configure method, the configuration instances will be available from the dependency injection framework as instances of IOptions<T>, where T is a template parameter of the type passed to Configure— as per this example, IOptions<LoggingSettings>.
The IOptions<T> interface specifies a Value property by which we can access the underlying instance that was passed or set in Configure. The good thing is that this is dynamically executed at runtime if—and only if—it is actually requested, meaning no binding from configuration to the POCO class will occur unless we explicitly want it.
A final note: before using Configure, we need to add support for it to the services collection as follows:
services.AddOptions();
For this, the Microsoft.Extensions.Options NuGet package will need to be added first, which will ensure that all required services are properly registered.
Retrieving named configuration options
When we register a POCO configuration by means of the Configure family of methods, essentially we are registering it to the dependency injection container as IOption<T>. This means that whenever we want to have it injected, we can just declare IOption<T>, such as IOption<LoggingSettings>. But if we want to use named configuration values, we need to use IOptionsSnapshot<T> instead. This interface exposes a nice Get method that takes as its sole parameter the named configuration setting, as follows:
public HomeController(IOptionsSnapshot<LoggingSettings> settings)
{
var elasticsearchSettings = settings.Get("Elasticsearch");
var consoleSettings = settings.Get("Console");
}
You must remember that we registered the LoggingSettings class through a call to the Configure method, which takes a name parameter.
Reloading and handling change notifications
You may remember that when we talked about the file-based providers, we mentioned the reloadOnChange parameter. This sets up a file-monitoring operation by which the operating system notifies .NET when the file's contents have changed. Even if we don't enable that, it is possible to ask the providers to reload their configuration. The IConfigurationRoot interface exposes a Reload method for just that purpose, as illustrated in the following code snippet:
var cfg = builder.Build(); cfg.Reload();
So, if we reload explicitly the configuration, we're pretty confident that when we ask for a configuration key, we will get the updated value in case the configuration has changed in the meantime. If we don't, however, the APIs we've already seen don't ensure that we get the updated version every time. For that, we can do either of the following:
- Register a change notification callback, so as to be notified whenever the underlying file content changes
- Inject a live snapshot of the data, whose value changes whenever the source changes too
For the first option, we need to get a handle to the reload token, and then register our callback actions in it, as follows:
var token = cfg.GetReloadToken(); token.RegisterChangeCallback(callback: (state) => { //state will be someData //push the changes to whoever needs it }, state: "SomeData");
For the latter option, instead of injecting IOptions<T>, we need to use IOptionsSnapshot<T>. Just by changing this, we can be sure that the injected value will come from the current, up-to-date configuration source, and not the one that was there when the configuration object was created. Have a look at the following code snippet for an example of this:
public class HomeController : Controller { private readonly LoggingSettings _settings; public HomeController(IOptionsSnapshot<LoggingSettings> settings) { _settings = settings.Value; } }
It is safe to always use IOptionsSnapshot<T> instead of IOptions<T> as the overhead is minimal.
Running pre- and post-configuration actions
There's a new feature since ASP.NET Core 2.0: running pre- and post-configuration actions for configured types. What this means is, after all the configuration is done, and before a configured type is retrieved from dependency injection, all instances of registered classes are given a chance to execute and make modifications to the configuration. This is true for both unnamed as well as named configuration options.
For unnamed configuration options (Configure with no name parameter), there is an interface called IConfigureOptions<T>, illustrated in the following code snippet:
public class PreConfigureLoggingSettings : IConfigureOptions<LoggingSettings>
{
public void Configure(LoggingSettings options)
{
//act upon the configured instance
}
}
And, for named configuration options (Configure with the name parameter), we have IConfigureNamedOptions<T>, as illustrated in the following code snippet:
public class PreConfigureNamedLoggingSettings : IConfigureNamedOptions<LoggingSettings>
{
public void Configure(string name, LoggingSettings options)
{
//act upon the configured instance
}
public void Configure(LoggingSettings options)
{
}
}
These classes, when registered, will be fired before the delegate passed to the Configure method. The configuration is simple, as can be seen in the following code snippet:
services.ConfigureOptions<PreConfigureLoggingSettings>();
services.ConfigureOptions<PreConfigureNamedLoggingSettings>();
But there's more: besides running actions before the configuration delegate, we can also run afterward. Enter IPostConfigureOptions<T>—this time, there are no different interfaces for named versus unnamed configuration options' registrations, as illustrated in the following code snippet:
public class PostConfigureLoggingSettings : IPostConfigureOptions<LoggingSettings>
{
public void PostConfigure(string name, LoggingSettings options) { ... }
}
To finalize, each of these classes is instantiated by the dependency injection container, which means that we can use constructor injection! This works like a charm, and can be seen in the following code snippet:
public PreConfigureLoggingSettings(IConfiguration configuration) { ... }
This is true for IConfigureOptions<T>, IConfigureNamedOptions<T>, and IPostConfigureOptions<T> as well.
And now, let's see some of the changes from previous versions.
- Learning Neo4j
- Go Web編程
- 大學計算機基礎(第三版)
- 差分進化算法及其高維多目標優化應用
- 你不知道的JavaScript(中卷)
- AutoCAD VBA參數化繪圖程序開發與實戰編碼
- Python漫游數學王國:高等數學、線性代數、數理統計及運籌學
- 軟件測試技術指南
- Java EE 7 Performance Tuning and Optimization
- 運用后端技術處理業務邏輯(藍橋杯軟件大賽培訓教材-Java方向)
- RealSenseTM互動開發實戰
- Swift 4從零到精通iOS開發
- 大話Java:程序設計從入門到精通
- Building Business Websites with Squarespace 7(Second Edition)
- JavaScript悟道