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
<PackageReference Include="MicrosoftExtensions.Options.DedupChangeExtensions" Version="1.1.1" />
<PackageVersion Include="MicrosoftExtensions.Options.DedupChangeExtensions" Version="1.1.1" />
<PackageReference Include="MicrosoftExtensions.Options.DedupChangeExtensions" />
paket add MicrosoftExtensions.Options.DedupChangeExtensions --version 1.1.1
#r "nuget: MicrosoftExtensions.Options.DedupChangeExtensions, 1.1.1"
#:package MicrosoftExtensions.Options.DedupChangeExtensions@1.1.1
#addin nuget:?package=MicrosoftExtensions.Options.DedupChangeExtensions&version=1.1.1
#tool nuget:?package=MicrosoftExtensions.Options.DedupChangeExtensions&version=1.1.1
MicrosoftExtensions.Options.DedupChangeExtensions
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:
- File system watchers can fire multiple events for a single file modification
- Text editors may perform multiple write operations when saving files
- 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:
- Computing a hash of the serialized options object when the callback is first registered
- Computing a new hash each time a change notification occurs
- Only invoking your callback if the hash values differ
- 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:
- Serialization: Objects are serialized using
BinaryFormatter
to capture their complete state - Hashing: The serialized data is hashed using SHA-1 to create a compact fingerprint
- Comparison: Hash values are compared using constant-time comparison to detect changes
- 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 monitorlistener
: 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.
Related Issues
- ASP.NET Core issue #2542 - Original discussion about duplicate change notifications
- Configuration issue #624 - Related configuration reload issues
Alternatives
If this library doesn't meet your needs, consider these alternatives:
- Delay-based approach: Add a delay before processing changes (as suggested in the original issue)
- Manual deduplication: Implement your own hash-based or timestamp-based deduplication
- Framework solutions: Wait for potential framework-level solutions in future .NET releases
Product | Versions 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. |
-
.NETFramework 4.6.2
- Microsoft.Extensions.Options (>= 2.1.0)
- System.ValueTuple (>= 4.4.0)
-
.NETStandard 2.0
- Microsoft.Extensions.Options (>= 2.1.0)
-
.NETStandard 2.1
- Microsoft.Extensions.Options (>= 9.0.3)
-
net8.0
- Microsoft.Extensions.Options (>= 9.0.3)
-
net9.0
- Microsoft.Extensions.Options (>= 9.0.3)
- System.Runtime.Serialization.Formatters (>= 4.3.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
# 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.