In classic ASP.NET and ASP.NET MVC, application settings and configuration were much easier. There was a global configuration file—web.config—and, within it, there was a section named appSettings that could receive flat name/value pairs of string data. Once read, an application setting had to be converted to the most appropriate type, whether numeric, Boolean or date. Any change to any sections of the web.config file, including the appSettings section, caused the application to restart, and restarting the application would reload up-to-date settings.
It was not perhaps super-optimized—any change caused a restart—but it was super-simple. To be precise, one could also offload the entire subtree of application settings to an external configuration file. In this case, any changes to the configuration would not cause a restart but, on the other hand, leave unsolved the problem of reloading the updated configuration tree!
What about ASP.NET Core?
If you check out the Microsoft documentation, you might be frightened by the number of possible ways you can manage configuration data, whether in reading, writing, reloading, composing, filtering, injecting and the like.
The main purpose of this article is to clarify things by illustrating the most common application scenarios rather than just going through the full list of available options.
Understanding the Configuration DOM
The configuration of an ASP.NET Core application is based on a list of name-value pairs collected at runtime from a variety of data sources—primarily, but not necessarily, one or more JSON files.
All loaded values are composed into a single container. The container is not necessarily a flat dictionary of attributes and related values. Collected data can have any level of nesting and can be hierarchical as well. The root container is an object that implements the IConfigurationRoot
interface. Configuration data is commonly built in the constructor of the startup class.
public class Startup { public IConfigurationRoot Configuration { get; } public Startup(IHostingEnvironment env) { var dom = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json") .Build(); // Save the configuration DOM Configuration = dom; // Next tasks: // - Load the config data into a POCO class // - Share the POCO class with the rest of the app } }
The constructor needs to be injected into the IHostingEnvironment
reference to set the base path to locate all the JSON file(s) used to populate the configuration Document Object Model (DOM).
The ConfigurationBuilder
class is responsible for aggregating configuration values together and building the DOM. The aggregated data should be saved within the startup class to be used later during the initialization of the pipeline. As mentioned, the DOM results from the combination of values that can possibly come from multiple data sources. Here’s an example:
var dom = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json") .AddJsonFile("appcredentials.json") .AddJsonFile("appui.json") .AddEnvironmentVariables() .AddInMemoryCollection( new Dictionary<string, string> { {"Timezone", "+1"} }) .Build();
The DOM is populated progressively, meaning that data providers are called in the same sequence in which they are concatenated to the configuration builder, and each is given a chance to override entries previously set. If the same entry, say Timezone
, is contributed by multiple providers, then the last wins.
You can import settings from any number of JSON files, and each JSON file can have its own structure. Note that JSON files support a special naming convention:
file-name.[Environment].json
The [Environment]
placeholder refers to the current environment name. For example, if the application is running in Development mode, then a file linked to the configuration tree with the following name is loaded.
appsettings.development.json
The same file, instead, would be ignored if the application is running in Staging or Production mode. Note also that the overriding rule holds, and the development settings file loaded later has still the chance to override any existing settings.
What if a JSON file is missing? The actual effect depends on the exact syntax used to invoke the method AddJsonFile
. As in the code snippet above—no additional parameter but the file name—an exception would be thrown as the file is considered mandatory. The following syntax, instead, makes it optional.
var dom = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true);
The AddEnvironmentVariables
methods add all known environment variables to the DOM and AddInMemoryCollection
adds the specified dictionary.
Direct Access to the Configuration DOM
To read configuration data programmatically, you can use an indexer syntax and specify a case-insensitive path string that points to the information you want to read. To delimit properties in a hierarchical schema, you use the colon (:) symbol. For example, consider the following JSON file:
{ "print" : { "pageFormat" : "A4", "color": "true" }, "grid" : { "sorting" : "false", "search" : "false" } }
The simplest way to read the pageFormat
setting is the following.
var pageFormat = Configuration["print:pageformat"];
It is important to note that, by default, the setting is returned as a plain string and must be programmatically converted to its actual concrete type before further use. There’s also a strongly-typed API, however.
var fmt = Configuration.GetValue<int>("print:pageformat");
The GetSection
method also lets you select an entire configuration subtree where you can act on using both the indexer and the strongly-typed API.
var fmt = Configuration.GetSection("print") .GetValue<int>("pageformat");
Note that all the methods for direct access work on the DOM as a whole, regardless of the actual source of the data, whether JSON, memory, command line or whatever.
Loading the Configuration in a POCO Class
Aside for different syntax and the support for multiple data providers (including those you can write yourself for reading from your own data sources, such as database tables), the ASP.NET Core configuration API is so far functionally equivalent to that of the old ASP.NET. However, here’s a first relevant difference—loading settings into a C# class.
In classic ASP.NET, it was possible, but it was entirely up to you. In ASP.NET Core, instead, you can use the Bind
method on the configuration root object.
var appSettings = new GlobalAppSettings(); Configuration.Bind(appSettings);
The net effect is that the freshly created instance of the POCO
class of your choice (GlobalAppSettings
in the code snippet) is automatically populated with all entries in the configuration DOM that match—by name—the public interface of the class. The binding rules are the same as in the controller’s model binding layer.
Once the custom POCO
configuration class has been populated, you can run your own validation code to make sure that only valid settings are flowing into the application. Note that the POCO
class is your class, meaning that you can give it all the helper methods you think you need.
Sharing the Configuration Settings Across the Application
The final step to conclude the first round of application settings in ASP.NET Core 3.0 is sharing the configuration POCO
class with the rest of the application. In ASP.NET Core, the recommended approach is using the native Dependency Injection layer. All you need to do is adding the freshly created (and validated) instance of the GlobalAppSettings
class as a singleton.
var appSettings = new GlobalAppSettings(); Configuration.Bind(appSettings); services.AddSingleton(appSettings);
Next up, a controller class that needs to process configuration data will only have to import via Dependency Injection the global singleton instance of the GlobalAppSettings
class.
public DemoController(GlobalAppSettings settings) { ... }
Another equally valid option consists of creating your own singleton.
public class GlobalAppSettings { public static GlobalAppSettings Instance = new GlobalAppSettings(); public GlobalAppSettings() { : Instance = this; } }
In this latter case, you don’t need to inject and import any reference to GlobalAppSettings
.
var settings = GlobalAppSettings.Instance;
After leaving the boundaries of the startup class, the singleton will be up and running and fully initialized. A plain reference to the Instance
property will be enough to read it. Being a configuration object, there should be no need to update it during the course of the program. If not, well, that could become an issue as updates may occur simultaneously.
Note that if you don’t like loading the configuration DOM into an aptly created POCO
class, you can still share the configuration DOM with the rest of the application by simply passing the IConfigurationRoot
object via the ASP.NET Dependency Injection system.
Moving One Step Further
So far so good. What’s been achieved is exactly the same as in old ASP.NET—just done better and with less effort. In other words, at this stage, the ASP.NET Core application can load all of its configuration settings from a variety of data sources, compose all entries in a single DOM, load it into C# POCO
class and share either via a singleton in the DI system or as a handmade singleton object.
The ASP.NET Core documentation presents a slightly different way to deal with the application configuration data. It achieves the same purpose, of course, but does that through an additional layer of abstraction—the IOptions<T>
wrapper. Say you have just built your configuration DOM and are ready to load it into a POCO
class. Here’s the alternative code you can use. Needless to say, the code below belongs to the ConfigureServices
method of the startup class.
services.AddOptions(); services.Configure<GlobalAppSettings>(Configuration);
The net effect of this code is double. First, it maps the configuration DOM to a freshly created instance of the GlobalAppSettings
class and, second, it shares the settings object through the Dependency Injection system wrapped up in an IOptions<GlobalAppSettings>
container. As a result, to access the settings object from within a controller class, you now need some slightly different code:
public DemoController( IOptions<GlobalAppSettings> options) { Settings = options.Value; ... }
The controller class receives an IOptions<T>
object and must dereference it to the actual configuration class via a call to the Value
property.
Should you use IOptions<T>
? And, if yes, why?
Frankly, there’s no clear reason for using IOptions<T>
except that it is the most commonly discussed scenario. A couple of (minor) differences with the plain singleton scenario discussed earlier are:
- It implements a sort of lazy loading of the configuration model. The actual mapping of the configuration data onto the embedded object occurs upon the first ever call to the property
Value
. - It provides a built-in mechanism to validate the configuration class.
Both points can be read as good points or bad points. It’s all about your perspective of things. For sure, by using IOptions<T>
or the direct settings class, you won’t lose any of your programming power and going through IOptions<T>
makes it slightly longer.
My personal idea is that the ASP.NET Core team first devised a more abstract approach to the whole topic of configuration data based on IOptions<T>
and later realized that at least the most basic scenario—shared and immutable singleton—could have been implemented more simply, and they added the Bind
method on the configuration root object.
What If You Want to Reload Settings?
As mentioned at the beginning of the article, in old ASP.NET, very occasionally someone cared about reloading settings on the fly without restarting the application. It is technically possible though. It only requires that you expose some dedicated endpoint that can be invoked (maybe even from the address bar with no dedicated admin UI) which will re-read the configuration sources and refreshes the current memory status. The application will use updated settings starting with the next request.
In ASP.NET Core, there’s also some built-in support for this scenario, and it passes through a couple of interfaces conceptually analogous to IOptions<T>
. They are IOptionsSnapshot<T>
and IOptionsMonitor<T>
.
The former works like IOptions<T>
except for one aspect. It doesn’t cache the configuration as an immutable singleton loaded at the application startup but reloads it for every single request. Put another way, the instance of the T
type is shared as a singleton if you use IOptions<T>
and is shared as a scoped object—singleton for the lifetime of the current request—if you use IOptionsSnapshot<T>
.
A solution based on IOptionsSnapshot<T>
has the power of getting up-to-date settings the first request after someone modifies the JSON file from which settings are loaded or one of the in-memory values or one of the environment variables.
What about IOptionsMonitor<T>
instead?
If used instead of IOptions<T>
, it still maintains an immutable singleton but adds some additional capabilities. In particular, if the JSON file(s) from which configuration is loaded have a file watcher mechanism attached, the options monitor will be notified and will react reloading the content from the JSON file(s) upon changes. To enable file watching, you need to proceed as follows:
var dom = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
In the end, if you wish that the ASP.NET Core application silently and automatically starts using updated settings as soon as possible, then use IOptionsMonitor<T>
and make sure you add JSON files to the configuration DOM with the reloadOnChange
flag turned on. Finally, notice that the configuration root object still has a Reload
method that can be programmatically invoked to force a DOM to reload and refresh.
Summary
Overall, the configuration API in ASP.NET Core (including the latest 3.0 version) is richer than in the past. However, the basic things you might want to do are easy to accomplish and don’t take more than just a few lines of code. At the same time, the richness of the new API results in a long list of features that may or may not be used commonly and may even generate confusion when a developer visits the official ASP.NET Core web site. The ideal approach is to learn the absolute minimum you need to know (which means how to load the configuration as an immutable singleton in a C# POCO
class) and then look around in case more is required or would be good to have. Chances are that all you need is already there!
The post ASP.NET Core 3.0 Configuration Factsheet appeared first on Simple Talk.
from Simple Talk http://bit.ly/2GzB0L8
via
No comments:
Post a Comment