Application configuration
.NET Core C# Dependency Injection Patterns Visual Studio

How to Read Configuration Changes within .NET Core

Welcome to today’s post.

In today’s post I will be discussing how to configure a .NET Core application so that use configuration settings that change during the lifetime of the running of the application can be read from within application dependencies.

In a previous post I showed how an application reads configurations using the IOptions pattern. With the IOptions pattern we used an object with a singleton lifetime to access configuration settings from the appSettings.json configuration file. During the lifetime of the application starting and exiting, the configuration settings will be unchanged even if the settings are modified and changed within the configuration file.

We can amend the options pattern we use to allow us to read settings that change for each request. In this case, we can read settings that change for each .NET Core API request. This would be useful for a long running service where the configurations may have to be changed due to a configuration change request. An example might be changing a polling interval for a service due to increasing frequency of demand for a worker process to be executed.

The IOptionsSnapshot Pattern

The interface we use to allow us to read changed settings are instances of the IOptionsSnapshot pattern. This will be similar usage to the IOptions interface but instead of a singleton lifetime, it will have a transient or scoped lifetime. When the dependency class is registered and instanced with a scoped lifetime, the options instance that is injected into a class constructor will read settings values that are current at the time the dependent class instance is created during a request. When the options instance is created with a transient lifetime, the options instance will take settings that are created within a dependent class method block.

I will go through an example within a .NET Core API application to demonstrate this.

In a previous post I showed how to create a configured scheduled hosted service. The service I created was a timer task that executes every 60 seconds. I will show how to bind (using the IOptionsSnapshot pattern) the timer service with a configured polling value within the appSettings.json configuration file. I will also show how to read the polling value within the timer hosted service when it changes within the configuration file.

Binding a Snapshot Option Pattern to a Hosted Service

First, I will show how to bind the snapshot option pattern within the hosted service.

In the application startup ConfigureServices() method we add the app settings file to our configuration provider as shown:

var configBuilder = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);

Notice that we have set the configuration to load when changes occur to the file by setting the reloadOnChange to true.

Recall our JSON configuration file is structured with a ScheduleSettings section as shown:

{
  "ScheduleSettings": {
    "pollingInterval": 20
  },
  …
}

Next, we setup the dependency injection of the timer hosted service as follows:

services.AddHostedService<TimedHostedService>();

The configuration class we use to bind our configuration key and value for pollingInterval is shown:

public class AppSnapshotConfiguration
{
    public int pollingInterval { get; set; }
}

Import the following namespace for the options extension pattern:

using Microsoft.Extensions.Options;

The options provider binds configurations from the configuration section “ScheduleSettings” within the configuration file to the configuration class as shown:

services.AddOptions<AppSnapshotConfiguration>()
    .Bind(config.GetSection("ScheduleSettings"));

Storing Updated Configuration Settings

Next, we create a basic class that will be created within a scope to hold the updated configuration polling value using the IOptionsSnapshot pattern:

public interface IMutableConfiguration
{
    public int pollingInterval { get; }
}

public class MutableConfiguration: IMutableConfiguration
{
    private int _pollingInterval { get; }

    public int pollingInterval 
    {
        get { return this._pollingInterval; }
    }

    public MutableConfiguration(
        IOptionsSnapshot<AppSnapshotConfiguration> snapshotOptionsAccessor)
    {
        IOptionsSnapshot<AppSnapshotConfiguration> 
            _snapshotOptionsAccessor = snapshotOptionsAccessor;
        _pollingInterval = 
            _snapshotOptionsAccessor.Value.pollingInterval;
    }
}

We next bind the above interface and class to the IOC dependency injection service collection:

services.AddTransient<IMutableConfiguration, MutableConfiguration>();

In our timed hosted service, we require the initial value of the polling configuration value, so we make the following changes to integrate the IOptions singleton pattern into the service though the constructor:

internal class TimedHostedService : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private Timer _timer;
    private readonly AppConfiguration _schedulingOptions;
    private int _pollingInterval;
    private readonly IServiceScopeFactory _serviceScopeFactory;

    public TimedHostedService(ILogger<TimedHostedService> logger,
        IServiceProvider serviceProvider,
        IServiceScopeFactory serviceScopeFactory,
        IOptions<AppConfiguration> optionsAccessor)
    {
        _serviceScopeFactory = serviceScopeFactory;
        _logger = logger;
        _schedulingOptions = optionsAccessor.Value;
        this.InitPollingInterval(_schedulingOptions.pollingInterval);
    }

    private void InitPollingInterval(int updatedInterval)
    {
        try
        {
            _pollingInterval = updatedInterval;
            _logger.LogInformation(
                $"Polling Interval Currently Set To: { _pollingInterval }");

            if (_pollingInterval > 180)
            {
                _logger.LogInformation(
                    $"Polling Interval { _pollingInterval } Is Too High!");
                throw new ArgumentException("Polling interval is too high");
            }
        }
        catch (Exception ex)
        {
            _pollingInterval = 60;
        }
    }
    ...
}

In the task execution method, we output the polling interval value and start the timer with the current timer duration:

public Task StartAsync(CancellationToken cancellationToken)
{
    _logger.LogInformation("Timed Background Service is starting.");

    _logger.LogInformation($"Polling Interval Set To: { _pollingInterval }");

    _timer = new Timer(DoWork, null, TimeSpan.Zero, 
        TimeSpan.FromSeconds(_pollingInterval));

    return Task.CompletedTask;
}

In the constructor, notice that I injected an instance of IServiceScopeFactory into our service class. The reason for this is that I will be using this factory to create a scope that will be used to obtain an instance of our mutable configuration class.

The timer callback event that executes obtains an instance to the IMutableConfiguration interface through the application’s IOC service collection. The polling value is then obtained from the updated configuration file from an instance of the MutableConfiguration class and set to the timer service internal polling property _pollingInterval.

private void DoWork(object state)
{
    _logger.LogInformation("Timed Background Service is working.");

    using (var scopedConfig = _serviceScopeFactory.CreateScope())
    {
        var service = scopedConfig.ServiceProvider
            .GetService<IMutableConfiguration>();
        this.InitPollingInterval(service.pollingInterval);
    }
}

The options snapshot provider works under a transient or scoped lifetime to retrieve the current value within the configuration file AFTER the application has started and will retrieve the value on subsequent creations of the MutableConfiguration class.

Folder Location of Changed Application Settings

When the application is built and run, the configuration settings will be copied over to the

\bin\Debug\netcoreapp3.1

subfolder within the project application folder.

To test if the changes are reflected within the hosted service, we open the appSettings.json file within the above subfolder and make two changes to the polling interval.

The first change is to amend 40 to a value of 30.

The second change is to amend 30 to a value of 20.

Note: The options snapshot pattern only checks application settings changes within the appSettings.json file within the OUTPUT directory, NOT in the project directory!

Below is a screenshot of the console debug showing the options values being read from the modified configuration file settings:

Be aware that the following conditions will result in the IOptionsSnapshot provider NOT providing updated values to the application:

  1. When the configuration provider property reloadOnChange is set to false.
  2. When IOptions is used within the constructor of the MutableConfiguration class instead of IOptionsSnapshot.

Note: If we do not enable the configuration to reload on a file change, then even if we were to inject the options snapshot interface pattern into our configuration class, the changes would NOT be readable! We would still get the original values of the configuration settings as if it were a singleton!

We have seen how to configure a .NET core application to read changing configuration values.  This configuration pattern could be quite useful in production environments when application configurations can change.

That is all for today’s post.

I hope you found this post useful and informative.

Social media & sharing icons powered by UltimatelySocial