官术网_书友最值得收藏!

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");
Don't forget that the value that you want to convert needs to be convertible from a string; in particular, it needs to have TypeConverter defined for that purpose, which all .NET Core primitive types do. The conversion will take place using the current culture.

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"
            }
        }
    }
}
Not all providers are capable of handling configuration sections or handle them in the same way. In XML, each section corresponds to a node; for INI files, there is a direct mapping; and for the Azure Key Vault, user secrets, memory (dictionaries), and providers, sections are specified as keys separated by colons (for example, ASPNET:Variable, MyApp:Variable, Data:Blog:ConnectionString, and more). For environment variables, they are separated by double underscores ( __). The example Registry provider I showed earlier does not, however, support them.

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; } }
LogLevel comes from the Microsoft.Extensions.Logging namespace.

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.

Don't forget that an error will occur if—and only if—a configuration value cannot be bound, either directly as a string or through TypeConverter to the target property in the Plain Old CLR Object ( POCO) class. If no such property exists, it will be silently ignored. The TypeConverter class comes from the System.ComponentModel NuGet package and namespace.

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);
Since version 2.0, ASP.NET Core automatically registers the configuration object ( IConfiguration) with the dependency injection framework.

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.

主站蜘蛛池模板: 博罗县| 星座| 博白县| 清流县| 姜堰市| 东至县| 霍林郭勒市| 石河子市| 玛沁县| 阳江市| 安顺市| 河南省| 安吉县| 新昌县| 安义县| 航空| 永善县| 闻喜县| 永顺县| 南皮县| 仪陇县| 南通市| 喜德县| 峡江县| 东阳市| 马尔康县| 丰顺县| 柳江县| 若尔盖县| 江油市| 色达县| 枣强县| 永安市| 仁化县| 乌兰浩特市| 雅安市| 华坪县| 广宁县| 习水县| 当阳市| 汶上县|