ASP.NET Core 3.0 Configuration Factsheet

There are many options to consider when managing the settings and configuration for ASP.NET Core applications. In this article, Dino Esposito explains the most common scenarios for configuration of these applications.

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.

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:

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:

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.

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.

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:

The simplest way to read the pageFormat setting is the following.

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.

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.

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.

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.

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.

Another equally valid option consists of creating your own singleton.

In this latter case, you don’t need to inject and import any reference to GlobalAppSettings.

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.

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:

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:

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!