This project helps prevent duplicated change callbacks for `IOptionsMonitor`. Provides deduplication extensions to reduce noise in configuration change notifications and improve application performance.
Deps
7
Install Size
—
Vulns
✓ 0
Published
Aug 22, 2025
$ dotnet add package MicrosoftExtensions.Options.DedupChangeExtensionsThis library provides extension methods for IOptionsMonitor<T> to prevent duplicate change notifications. It solves the common problem where configuration change callbacks are fired multiple times for the same logical configuration change, which can lead to unnecessary processing and performance issues.
When using IOptionsMonitor<T> in ASP.NET Core applications, change callbacks are often triggered multiple times in quick succession for a single configuration file change. This happens because:
ChangeToken.OnChange mechanism doesn't deduplicate notificationsThis issue is well-documented in ASP.NET Core issue #2542, where developers reported seeing their change handlers called twice or more for each configuration update.
This library implements hash-based deduplication to ensure your change callbacks are only invoked when the configuration values have actually changed. It works by:
Install the package via NuGet:
dotnet add package MicrosoftExtensions.Options.DedupChangeExtensions
Or via Package Manager Console:
Install-Package MicrosoftExtensions.Options.DedupChangeExtensions
using Microsoft.Extensions.Options;
public class MyService
{
private readonly IDisposable _changeSubscription;
public MyService(IOptionsMonitor<MyOptions> optionsMonitor)
{
// Register for deduplicated change notifications
_changeSubscription = optionsMonitor.OnChangeDedup(options =>
{
// This callback will only fire when MyOptions actually changes
Console.WriteLine("Configuration changed!");
HandleConfigurationChange(options);
});
}
private void HandleConfigurationChange(MyOptions options)
{
// Your configuration change logic here
}
public void Dispose()
{
_changeSubscription?.Dispose();
}
}
public class MyService
{
private readonly IDisposable _changeSubscription;
public MyService(IOptionsMonitor<DatabaseOptions> optionsMonitor)
{
// Monitor a specific named options instance
_changeSubscription = optionsMonitor.OnChangeDedup("ProductionDB", (options, name) =>
{
Console.WriteLine($"Configuration '{name}' changed!");
ReconfigureDatabase(options);
});
}
private void ReconfigureDatabase(DatabaseOptions options)
{
// Reconfigure your database connection
}
public void Dispose()
{
_changeSubscription?.Dispose();
}
}
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// Configure your options as usual
services.Configure<MyOptions>(Configuration.GetSection("MySection"));
// Register your service that uses deduplicated change notifications
services.AddSingleton<MyService>();
}
}
The deduplication mechanism uses the following approach:
BinaryFormatter to capture their complete stateInterlocked.Exchange for thread-safe hash updates// Simplified version of the internal process
object options = optionsMonitor.Get(name);
byte[] serializedData = BinaryFormatter.Serialize(options);
byte[] hash = SHA1.ComputeHash(serializedData);
// Compare with previous hash
if (!previousHash.SequenceEqual(hash))
{
// Invoke your callback
listener(options, name);
previousHash = hash;
}
OnChangeDedup<TOptions>(Action<TOptions> listener)Registers a change callback for the default named options instance that only fires when values change.
Parameters:
listener: The callback to invoke when options changeReturns: IDisposable to unregister the callback
OnChangeDedup<TOptions>(string name, Action<TOptions, string> listener)Registers a change callback for a specific named options instance that only fires when values change.
Parameters:
name: The name of the options instance to monitorlistener: The callback to invoke when options changeReturns: IDisposable to unregister the callback
BinaryFormatter for serialization, which has some overhead. This is typically negligible compared to configuration reload operations.This library supports the following target frameworks:
It's compatible with:
All methods in this library are thread-safe and can be called concurrently from multiple threads. The internal hash storage uses atomic operations to ensure consistency.
Contributions are welcome! Please feel free to submit issues and pull requests.
LGPL-3.0-or-later WITH LGPL-3.0-linking-exception. See the LICENSE for details.
If this library doesn't meet your needs, consider these alternatives: