MicrosoftExtensions.Options.DedupChangeExtensions 1.1.1

dotnet add package MicrosoftExtensions.Options.DedupChangeExtensions --version 1.1.1
                    
NuGet\Install-Package MicrosoftExtensions.Options.DedupChangeExtensions -Version 1.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MicrosoftExtensions.Options.DedupChangeExtensions" Version="1.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MicrosoftExtensions.Options.DedupChangeExtensions" Version="1.1.1" />
                    
Directory.Packages.props
<PackageReference Include="MicrosoftExtensions.Options.DedupChangeExtensions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MicrosoftExtensions.Options.DedupChangeExtensions --version 1.1.1
                    
#r "nuget: MicrosoftExtensions.Options.DedupChangeExtensions, 1.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MicrosoftExtensions.Options.DedupChangeExtensions@1.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MicrosoftExtensions.Options.DedupChangeExtensions&version=1.1.1
                    
Install as a Cake Addin
#tool nuget:?package=MicrosoftExtensions.Options.DedupChangeExtensions&version=1.1.1
                    
Install as a Cake Tool

MicrosoftExtensions.Options.DedupChangeExtensions

NuGet

This 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.

Problem Statement

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:

  1. File system watchers can fire multiple events for a single file modification
  2. Text editors may perform multiple write operations when saving files
  3. The underlying ChangeToken.OnChange mechanism doesn't deduplicate notifications

This 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.

Solution

This library implements hash-based deduplication to ensure your change callbacks are only invoked when the configuration values have actually changed. It works by:

  1. Computing a hash of the serialized options object when the callback is first registered
  2. Computing a new hash each time a change notification occurs
  3. Only invoking your callback if the hash values differ
  4. Updating the stored hash for future comparisons

Installation

Install the package via NuGet:

dotnet add package MicrosoftExtensions.Options.DedupChangeExtensions

Or via Package Manager Console:

Install-Package MicrosoftExtensions.Options.DedupChangeExtensions

Usage

Basic Usage (Default Options)

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();
    }
}

Named Options

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();
    }
}

Dependency Injection Setup

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>();
    }
}

How It Works

The deduplication mechanism uses the following approach:

  1. Serialization: Objects are serialized using BinaryFormatter to capture their complete state
  2. Hashing: The serialized data is hashed using SHA-1 to create a compact fingerprint
  3. Comparison: Hash values are compared using constant-time comparison to detect changes
  4. Thread Safety: Uses Interlocked.Exchange for thread-safe hash updates

Hash Calculation Process

// 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;
}

API Reference

Extension Methods

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 change

Returns: 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 monitor
  • listener: The callback to invoke when options change

Returns: IDisposable to unregister the callback

Performance Considerations

  • Serialization Overhead: The library uses BinaryFormatter for serialization, which has some overhead. This is typically negligible compared to configuration reload operations.
  • Memory Usage: Hash values (20 bytes for SHA-1) are stored per monitored options instance.
  • Thread Safety: All operations are thread-safe and use efficient atomic operations.

Compatibility

This library supports the following target frameworks:

  • .NET Standard 2.0 (for broad compatibility)
  • .NET Standard 2.1
  • .NET Framework 4.6.2
  • .NET 8.0
  • .NET 9.0

It's compatible with:

  • ASP.NET Core 2.0+
  • .NET Framework applications using Microsoft.Extensions.Options
  • Any application using the Microsoft.Extensions.Options package

Thread Safety

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.

Contributing

Contributions are welcome! Please feel free to submit issues and pull requests.

License

This project is licensed under the GNU General Public License v3.0. See the LICENSE file for details.

Alternatives

If this library doesn't meet your needs, consider these alternatives:

  1. Delay-based approach: Add a delay before processing changes (as suggested in the original issue)
  2. Manual deduplication: Implement your own hash-based or timestamp-based deduplication
  3. Framework solutions: Wait for potential framework-level solutions in future .NET releases
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 is compatible. 
.NET Framework net461 was computed.  net462 is compatible.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.1 114 7/16/2025
1.0.2 2,156 1/18/2018
1.0.1 1,293 1/8/2018
1.0.0 1,321 1/8/2018

# Release Notes
## Version 1.1.1
### New Features
- **Multi-Target Framework Support**: Added support for .NET 8.0 and .NET 9.0 alongside existing frameworks
- **Enhanced Type Safety**: Added nullable reference types support for .NET Standard 2.1+ and .NET Core targets
- **Comprehensive Documentation**: Added extensive XML documentation with detailed examples and security considerations
### Improvements
- **Modern C# Features**: Leveraged nullable annotations for better compile-time safety on supported frameworks
- **Performance Optimizations**: Improved memory usage patterns with `MemoryExtensions.SequenceEqual` for hash comparison on modern frameworks
- **Security Enhancements**:
- Added proper warning suppression for SYSLIB0011 (BinaryFormatter security warning)
- Enabled `EnableUnsafeBinaryFormatterSerialization` for .NET 9.0 with proper safeguards
- Enhanced null handling with defensive programming practices
### Framework Compatibility
- .NET Standard 2.0 (unchanged for broad compatibility)
- .NET Standard 2.1 (with nullable support)
- .NET Framework 4.6.2 (with System.ValueTuple dependency)
- .NET 8.0 (with modern APIs and optimizations)
- .NET 9.0 (with latest features and security configurations)
### Technical Improvements
- **Thread Safety**: Enhanced thread-local storage patterns for better concurrency
- **Hash Algorithm**: Continued use of SHA1 for hash-based deduplication with optimized implementation
- **Error Handling**: Improved null reference handling across all target frameworks
- **Code Quality**: Enhanced code documentation and inline comments for maintainability
### Migration Path
This version maintains full backward compatibility with previous releases while adding modern framework support. No breaking changes for existing users.
---
## Version 1.0.2 (2018-01-18)
### Bug Fixes
- Minor stability improvements and package metadata updates
---
## Version 1.0.1 (2017-2018)
### Bug Fixes & Improvements
- Improved reliability of hash-based comparison
- Enhanced thread safety for concurrent scenarios
---
## Version 1.0.0 (2017)
### Initial Release
- **Core Functionality**: Implemented hash-based deduplication for `IOptionsMonitor<T>` change callbacks
- **Problem Solved**: Addressed the issue documented in [ASP.NET Core issue #2542](https://github.com/aspnet/Home/issues/2542) where configuration change callbacks were fired multiple times for the same logical configuration change
- **Hash-Based Detection**: Used `BinaryFormatter` serialization followed by `SHA1` hashing for reliable change detection
- **Thread Safety**: Implemented thread-safe hash token comparison using `Interlocked.Exchange`
- **API Design**: Provided both named and default options monitoring with clean extension method API
### Key Features
- `OnChangeDedup<TOptions>()` extension methods for `IOptionsMonitor<T>`
- Support for both named options instances and default options
- Automatic deduplication prevents unnecessary callback invocations
- Thread-safe implementation suitable for high-concurrency scenarios
- Minimal performance overhead with efficient hash-based comparison
### Target Framework
- .NET Standard 2.0 for broad compatibility with .NET Framework and .NET Core
### Dependencies
- Microsoft.Extensions.Options (2.0.0+)
---
## Project History
### 2022-08-17: Project Migration
- Moved from standalone repository [hcoona/MicrosoftExtensions.Options.DedupChangeExtensions](https://github.com/hcoona/MicrosoftExtensions.Options.DedupChangeExtensions) to [OneDotNet monorepo](https://github.com/hcoona/OneDotNet)
- Integrated into unified build and packaging system
- Maintained full backward compatibility during migration
### Original Problem Statement
This library was created to solve a common issue in ASP.NET Core applications where `IOptionsMonitor<T>` change callbacks would be triggered multiple times in quick succession for a single configuration file change. This happened because:
1. File system watchers can fire multiple events for a single file modification
2. Text editors may perform multiple write operations when saving files
3. The underlying `ChangeToken.OnChange` mechanism doesn't deduplicate notifications
### Solution Approach
The library implements **hash-based deduplication** by:
1. Computing a hash of the serialized options object when the callback is first registered
2. Computing a new hash each time a change notification occurs
3. Only invoking the user callback if the hash values differ
4. Updating the stored hash for future comparisons
This ensures that duplicate notifications are filtered out while preserving legitimate configuration changes.
---
## License
This project is licensed under the GNU General Public License v3.0. See [LICENSE](https://www.gnu.org/licenses/gpl-3.0-standalone.html) for details.
## Contributing
This project is part of the OneDotNet ecosystem. For contributions, please refer to the main repository guidelines.