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

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<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; }
}
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
setInterval(() => {
fetch('Index/?handler=TenantName')
.then(res => res.json())
.then(data => {
document.getElementById('tenantNameDisplay').innerText = data;
});
}, 5000);
Sample 2
This project (IOptionsMonitorAzureSettingsApp) used two distinct methods. Rather than explaining the code fully, the following lays out the logic for the two pages.
To demonstrate OnChange, both pages show any changes on the front end. Use JavaScript to check for changes in the app settings file.
Index page
Setup in Program.cs using model AzureSettings.
"AzureSettings": {
"ConnectionString": "Data Source=Staging;Initial Catalog=Fleet;Integrated Security=True;Encrypt=False",
"TenantId": "161e59c7-97ce-4e56-84bf-b9568bc3ff4r"
}
builder.Services.Configure<AzureSettings>(builder.Configuration.GetSection("AzureSettings"));
builder.Services.Configure<AzureSettings>("TenantName", builder.Configuration.GetSection("TenantNameAzureSettings"));
OnGet calls a method to load settings from appsettings.json using private static variables to persist values across requests.
The following JavaScript code polls the backend (C#) for changes in the front end, which is why static variables are used.
setInterval(() => {
fetch('?handler=CheckForUpdate')
.then(response => response.json())
.then(data => {
if (data.updated) {
document.getElementById("changeNotification").innerText = data.message;
setTimeout(() => location.reload(), 3000); // Refresh page
}
});
}, 5000); // Check every 5 seconds
In the code above ?handler=CheckForUpdate matches C# code OnGetCheckForUpdate. OnGetCheckForUpdate uses conventional if statements to determine if there are any changes since the last polling.
Note
The code presented can easily be used in other pages.
Index1 page
Setup in Program.cs using model _AzureSettings1 _ reading Azure section in appsettings.json.
builder.Services.Configure<AzureSettings1>(builder.Configuration.GetSection("Azure"));
builder.Services.AddSingleton<SettingsMonitorService>();
builder.Services.AddHostedService<AzureWorker>();
Note
AzureWorker inherits from BackgroundService.
This page has significantly less code on the back end. Changes are detected from JavaScript, which invokes a method in another page, Settings.cshtml.cs. The majority of the code resides in SettingsMonitorService and AzureWorker.
The important concept for this page is that a hash is used to determine if a value has changed in appsettings.json.
SettingsMonitorService.cs ComputeSnapshot
private string ComputeSnapshot(AzureSettings1 settings)
{
var json = JsonSerializer.Serialize(settings, Options);
using var sha = SHA256.Create();
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(json));
return Convert.ToBase64String(hash);
}
And for getting the hash in SettingsMonitorService
public string GetSnapshotHash() => _lastSnapshot;
Which is exposed in Settings.cshtml.cs
public class SettingsModel(SettingsMonitorService monitor) : PageModel
{
public IActionResult OnGet()
{
return new JsonResult(new
{
snapshotHash = monitor.GetSnapshotHash()
});
}
}
Then in SettingsMonitorService.cs OnChange for AzureSettings1 model when triggered.
- Get the current hash
- Compare the current hash to the latter, if different we have a change
- Invoke an Action with current values.
public event Action<AzureSettings1>? SettingsChanged;
public SettingsMonitorService(IOptionsMonitor<AzureSettings1> monitor)
{
_current = monitor.CurrentValue;
_lastSnapshot = ComputeSnapshot(_current);
monitor.OnChange(updated =>
{
var newSnapshot = ComputeSnapshot(updated);
// Only invoke if the snapshot really changed
if (newSnapshot == _lastSnapshot) return;
_current = updated;
_lastSnapshot = newSnapshot;
SettingsChanged?.Invoke(_current);
});
}
Back in the frontend, JavaScript polls for changes. When changes are detected, write a message for notifying that a value has change and update the value for connection string or tenant properties.
Understanding the code presented
The best method to understand the code presented is:
- Set breakpoints and step through the code. Microsoft Visual Studio has the best tools for debugging code.
- If unfamiliar with services and BackgroundService, read Microsoft documentation.
Summary
Not every application needs change notifications, but some do when a change is made and it's not a good time to restart an application.