Why is the IOptionsMonitor OnChange callback method called twice instead of one time when monitoring XML config changes

Silent Noise 26 Reputation points
2024-01-14T19:31:08.8733333+00:00

I want to call another method every time the XML config file changes any value and is saved. This code works but the OnChange method is called twice each time:

If I attempt to use the implementation of IDisposable on the OnChange then the code does not work at all.

Example:

        var monitor = configurationMonitor.OnChange(config =>
        {
            ConfigChange();
        });
        monitor.Dispose();

I have tried so many variations of this code but cannot get the desire result. I prefer not to use the FileEWatcher class.

static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        builder.Configuration.AddXmlFile("appsettings.xml", optional: false, reloadOnChange: true);

        builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("Settings"));

        var app = builder.Build();

        var configurationMonitor = app.Services.GetRequiredService<IOptionsMonitor<AppSettings>>();          

        var monitor = configurationMonitor.OnChange(config =>
        {
            ConfigChange();
        });

        app.Run();
    }
.NET
.NET
Microsoft Technologies based on the .NET software framework.
4,100 questions
ASP.NET Core
ASP.NET Core
A set of technologies in the .NET Framework for building web applications and XML web services.
4,778 questions
C#
C#
An object-oriented and type-safe programming language that has its roots in the C family of languages and includes support for component-oriented programming.
11,288 questions
0 comments No comments
{count} votes

1 answer

Sort by: Most helpful
  1. Pinaki Ghatak 5,575 Reputation points Microsoft Employee
    2024-01-14T19:39:57.1766667+00:00

    Hello @Silent Noise The OnChange method being called twice could be due to a variety of reasons. Here are a few possibilities: File System Events: The file system might be raising multiple events for a single change, such as a write event followed by a close event. The OnChange method might be responding to each of these events. Multiple Subscriptions: There might be multiple subscriptions to the OnChange event, causing it to be triggered more than once.

    1. Focus Change: In some cases, changing the focus can trigger the OnChange event.

    To avoid the OnChange method being called twice, you could add a condition to check if the configuration has actually changed before calling ConfigChange(). Here’s a simple example of how you could implement this:

    AppSettings previousConfig = null;
    
    var monitor = configurationMonitor.OnChange(config =>
    {
        if (previousConfig == null || !config.Equals(previousConfig))
        {
            ConfigChange();
            previousConfig = config;
        }
    });
    

    In this code, ConfigChange() is only called if the configuration has actually changed. This should prevent OnChange from being called twice for the same configuration change. Please note that you would need to implement an appropriate Equals method for the AppSettings class for this to work correctly. Remember to test this solution thoroughly to ensure it works as expected in your specific use case. If the problem persists, you might want to consider using a debouncing technique (see below) to limit how often OnChange can be called.

    Debouncing technique

    This technique uses a timer to delay the execution of the ConfigChange() method until a certain amount of time has passed without any new changes. If a new change occurs before the timer expires, the timer is reset.

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    
    public class Program
    {
        static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);
            builder.Configuration.AddXmlFile("appsettings.xml", optional: false, reloadOnChange: true);
    
            builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("Settings"));
    
            var app = builder.Build();
    
            var configurationMonitor = app.Services.GetRequiredService<IOptionsMonitor<AppSettings>>();          
    
            var last = 0;
            Action<AppSettings> debouncedConfigChange = Debounce<AppSettings>(config => ConfigChange(config), 300);
    
            var monitor = configurationMonitor.OnChange(debouncedConfigChange);
    
            app.Run();
        }
    
        static Action<T> Debounce<T>(Action<T> func, int milliseconds = 300)
        {
            var last = 0;
            return arg =>
            {
                var current = Interlocked.Increment(ref last);
                Task.Delay(milliseconds).ContinueWith(task =>
                {
                    if (current == last) func(arg);
                    task.Dispose();
                });
            };
        }
    
        static void ConfigChange(AppSettings config)
        {
            // Your code here
        }
    }
    

    In this code, Debounce<T> is a method that returns a new function. This new function, when called, starts a timer that waits for the specified debounce time. If the function is called again before the timer expires, the timer is reset. If the timer expires without the function being called again, it calls the original function func. Please note that this is a simple implementation and might not cover all edge cases. Always make sure to test thoroughly to ensure it works as expected in your specific use case. If this answers your question, please tag as answered.


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.