ASP .NET Core IOptionsMonitor Onchange

Introduction Learn how to set up a listener for IOptionsMonitor, which allows an ASP.NET Core application to be run without restarting and provides pinpoint logic to assert that the monitored values are valid. Using code provided coupled with ValidateOnStart as presented in this article there is less chance of runtime issues. Some examples Is a connection string valid? Is a value in a specified range? If a property has a valid value e.g. a guid or a date are valid Requires Microsoft Visual Studio 2022 17.13.6 or any IDE that supports NET9 or higher. Bootstrap v5.3.3 or higher. Understanding code presented Some of the code presented may be new, depending on a developer's experience. The best way to move forward with the code presented is to study it, read Microsoft documentation, and set breakpoints to step through it to get a solid understanding of it, rather than copying and pasting code into a new or existing project. Using GitHub Copilot to explain code is another resource for understanding unfamiliar code. Using a browser developer tools console is also worth using to understand how JavaScript is used by setting breakpoints. AI GitHub Copilot and ChatGPT were used to assist in writing code. JetBrains AI Assistant was used to create all documentation. The tools were used not to figure out how to write something unknown but to save time. For example, writing documentation for a method without AI might take five or more minutes while AI does this in seconds. For code, the following would take perhaps ten minutes while writing a Copilot or ChatGPT prompt and response, five or fewer minutes. // Configure Serilog Log.Logger = new LoggerConfiguration() .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning) .MinimumLevel.Override("System", Serilog.Events.LogEventLevel.Warning) .MinimumLevel.Information() .WriteTo.Console() .CreateLogger(); builder.Host.UseSerilog(); // Load configuration with reload on change builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true); // Register IOptionsMonitor with reloading enabled builder.Services.Configure(builder.Configuration.GetSection("AzureSettings")); builder.Services.Configure("TenantName", builder.Configuration.GetSection("TenantNameAzureSettings")); // Register our services builder.Services.AddSingleton(); builder.Services.AddScoped(); Using AI is a great way to save time besides helping with tasks that a developer does not know how to write code for. Code overview To simplify the learning process, all the code provided shows how to detect changes in the project’s appsettings.json file for specific class properties. After detection happens, use separate methods to determine if values are valid; for example, use TryParse to determine if a GUID is valid, or for a connection string, write code to validate that a connection string can be used to open a connection. If invalid values are detected, log the issue and determine if the application can continue running. The first code sample is basic, while the second is a step up. A NuGet package CompareNETObjects is used in one project to assist for detecting if multiple changes at one time. Testing Testing, running one of the projects, opening appsettings.json, changing a tracked item, and saving. Diving in For the first example, project AzueSettingsOptionsMonitorSample, we are monitoring for changes to TenantName and ConnectionString properties in the following model. Backend code public class AzureSettings { public const string Settings = "AzureSettings"; public bool UseAdal { get; set; } public string Tenant { get; set; } public string TenantName { get; set; } public string TenantId { get; set; } public string Audience { get; set; } public string ClientId { get; set; } public string GraphClientId { get; set; } public string GraphClientSecret { get; set; } public string SignUpSignInPolicyId { get; set; } public string AzureGraphVersion { get; set; } public string MicrosoftGraphVersion { get; set; } public string AadInstance { get; set; } public string ConnectionString { get; set; } } The following code is in Program.cs configures the AzureSettings class with values from the application's configuration system. builder.Services.Configure(builder.Configuration.GetSection(AzureSettings.Settings)); builder.Services.Configure(builder.Configuration.GetSection(nameof(AzureSettings))); Index page backend In the constructor, dependency injection is used to access AzureSettings values in appsettings.json, followed by subscribing to OnChange for IOptionsMonitor. An alternative to using a lambda statement for OnChange is to create a method while, as presented, it is easy to determine the code flow. In both cases, OnTenantNameChanged and OnConnectionStringChanged c

Apr 19, 2025 - 16:42
 0
ASP .NET Core IOptionsMonitor Onchange

Introduction

Learn how to set up a listener for IOptionsMonitor, which allows an ASP.NET Core application to be run without restarting and provides pinpoint logic to assert that the monitored values are valid.

Using code provided coupled with ValidateOnStart as presented in this article there is less chance of runtime issues.

Some examples

  • Is a connection string valid?
  • Is a value in a specified range?
  • If a property has a valid value e.g. a guid or a date are valid

Requires

Understanding code presented

Some of the code presented may be new, depending on a developer's experience. The best way to move forward with the code presented is to study it, read Microsoft documentation, and set breakpoints to step through it to get a solid understanding of it, rather than copying and pasting code into a new or existing project.

  • Using GitHub Copilot to explain code is another resource for understanding unfamiliar code.
  • Using a browser developer tools console is also worth using to understand how JavaScript is used by setting breakpoints.

AI

  • GitHub Copilot and ChatGPT were used to assist in writing code.
  • JetBrains AI Assistant was used to create all documentation.

The tools were used not to figure out how to write something unknown but to save time. For example, writing documentation for a method without AI might take five or more minutes while AI does this in seconds. For code, the following would take perhaps ten minutes while writing a Copilot or ChatGPT prompt and response, five or fewer minutes.

// Configure Serilog
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Override("Microsoft", Serilog.Events.LogEventLevel.Warning)
    .MinimumLevel.Override("System", Serilog.Events.LogEventLevel.Warning)
    .MinimumLevel.Information()
    .WriteTo.Console()
    .CreateLogger();

builder.Host.UseSerilog();

// Load configuration with reload on change
builder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true);

// Register IOptionsMonitor with reloading enabled
builder.Services.Configure<AzureSettings>(builder.Configuration.GetSection("AzureSettings"));
builder.Services.Configure<AzureSettings>("TenantName", builder.Configuration.GetSection("TenantNameAzureSettings"));

// Register our services
builder.Services.AddSingleton<AzureService>();
builder.Services.AddScoped<SettingsService>();

Using AI is a great way to save time besides helping with tasks that a developer does not know how to write code for.

Code overview

To simplify the learning process, all the code provided shows how to detect changes in the project’s appsettings.json file for specific class properties. After detection happens, use separate methods to determine if values are valid; for example, use TryParse to determine if a GUID is valid, or for a connection string, write code to validate that a connection string can be used to open a connection. If invalid values are detected, log the issue and determine if the application can continue running.

The first code sample is basic, while the second is a step up.

A NuGet package CompareNETObjects is used in one project to assist for detecting if multiple changes at one time.

Testing

Testing, running one of the projects, opening appsettings.json, changing a tracked item, and saving.

Diving in

For the first example, project AzueSettingsOptionsMonitorSample, we are monitoring for changes to TenantName and ConnectionString properties in the following model.

Backend code

public class AzureSettings
{
    public const string Settings = "AzureSettings";
    public bool UseAdal { get; set; }
    public string Tenant { get; set; }
    public string TenantName { get; set; }
    public string TenantId { get; set; }
    public string Audience { get; set; }
    public string ClientId { get; set; }
    public string GraphClientId { get; set; }
    public string GraphClientSecret { get; set; }
    public string SignUpSignInPolicyId { get; set; }
    public string AzureGraphVersion { get; set; }
    public string MicrosoftGraphVersion { get; set; }
    public string AadInstance { get; set; }
    public string ConnectionString { get; set; }
}

appsettings.json pointing to focused properties

The following code is in Program.cs configures the AzureSettings class with values from the application's configuration system.

builder.Services.Configure<AzureSettings>(builder.Configuration.GetSection(AzureSettings.Settings));
builder.Services.Configure<AzureSettings>(builder.Configuration.GetSection(nameof(AzureSettings)));

Index page backend

In the constructor, dependency injection is used to access AzureSettings values in appsettings.json, followed by subscribing to OnChange for IOptionsMonitor.

An alternative to using a lambda statement for OnChange is to create a method while, as presented, it is easy to determine the code flow. In both cases, OnTenantNameChanged and OnConnectionStringChanged can still be used.

For a real application, both cases, OnTenantNameChanged and OnConnectionStringChanged, would include validation and code to determine if the application can continue or if any value will cause an uncontrolled runtime error.

The method OnGetTenantName is for demonstration to work with the JavaScript code in the front to display the changed tenant names in a span that checks for changes every five seconds.

public class IndexModel : PageModel
{
    private readonly IOptionsMonitor<AzureSettings> _azureSettings;

    private AzureSettings _azureSettingsIOptionsMonitor;

    [BindProperty]
    public required string TenantName { get; set; }


    public IndexModel(IOptionsMonitor<AzureSettings> azureSettings)
    {
        _azureSettings = azureSettings;
        _azureSettingsIOptionsMonitor = _azureSettings.CurrentValue;

        _azureSettings.OnChange(config =>
        {
            if (_azureSettingsIOptionsMonitor.TenantName != config.TenantName)
            {
                OnTenantNameChanged(config);
            }
            else if (_azureSettingsIOptionsMonitor.ConnectionString != config.ConnectionString)
            {
                OnConnectionStringChanged(config);
            }
        });
    }


    private void OnTenantNameChanged(AzureSettings azureSettings)
    {
        _azureSettingsIOptionsMonitor.TenantName = azureSettings.TenantName;
        TenantName = azureSettings.TenantName;
    }

      private void OnConnectionStringChanged(AzureSettings azureSettings)
    {
        _azureSettingsIOptionsMonitor.ConnectionString = azureSettings.ConnectionString;
    }
    public void OnGet()
    {
        TenantName = _azureSettingsIOptionsMonitor.TenantName;
    }

    [HttpGet]
    public IActionResult OnGetTenantName()
    {
        return new JsonResult(_azureSettings.CurrentValue.TenantName);
    }

}

Frontend code

The code is here so that a developer can see the changes that were detected. In JavaScript, setInterval fetch relies on OnGetTenantName to get the new value of the tenant’s name.

For those who copy and paste code, if a page name is not index, change it to the correct page name. For index we could also use fetch('/?handler=TenantName').

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

 class="main-content">
     class="container">
         class="text-center">IOptionsMonitor  OnChange
    
class="container text-center mt-3"> Tenant Name: id="tenantNameDisplay" class="fw-bold text-success">@Model.TenantName