ConfigurationScopedService 1.1.3
dotnet add package ConfigurationScopedService --version 1.1.3
NuGet\Install-Package ConfigurationScopedService -Version 1.1.3
<PackageReference Include="ConfigurationScopedService" Version="1.1.3" />
<PackageVersion Include="ConfigurationScopedService" Version="1.1.3" />
<PackageReference Include="ConfigurationScopedService" />
paket add ConfigurationScopedService --version 1.1.3
#r "nuget: ConfigurationScopedService, 1.1.3"
#:package ConfigurationScopedService@1.1.3
#addin nuget:?package=ConfigurationScopedService&version=1.1.3
#tool nuget:?package=ConfigurationScopedService&version=1.1.3
ConfigurationScopedService
A thread-safe, automated live-reload mechanism for .NET services in response to configuration changes.
💡 Rationale
This library solves the concurrency and data-race issues introduced by the default IOptionsMonitor implementation. (See Why IOptionsMonitor is Potentially Unsafe below).
Writing reliable code in highly concurrent environments is difficult. ConfigurationScopedService alleviates this pain by automating live service reloading using an architectural pattern: services should be completely reconstructed when configuration changes, rather than managing state changes internally.
Key Advantages
- Generic & Reusable: The reload logic is decoupled from your business logic.
- Immutable State: Services are written with the guarantee that initial conditions never change mid-execution.
- Cleaner Code: No need for complex resource re-initialization logic. Just dispose of assets naturally in your
Disposemethod. - Fewer Dependencies: Removes the need to inject
IOptions,IOptionsSnapshot, orIOptionsMonitorinto your service logic.
🚀 Introduction
This library introduces a new service lifetime scope: ConfigurationScoped.
The lifetime of a ConfigurationScoped service is bound to a specific configuration section:
- No changes: The service behaves effectively as a Singleton.
- Configuration changes: A new instance of the service is constructed and made available for all future resolutions.
Reference Counting
The library tracks active service usage via a reference-counting mechanism. When configuration updates, a new version of the service is instantly swapped in. The old version phases out and is automatically disposed of once its reference count hits zero (meaning all active scopes using it have finished).
Swapping Modes
You can configure how services swap when a configuration change is detected:
1. Non-Blocking (Default & Recommended)
- A new service instance is initialized.
- The new service is swapped in; the old instance's reference count decrements.
- If the old instance's count hits 0, it is immediately disposed.
- If the count is > 0, it is safely disposed later when active scopes finish.
- 👍 Pros: Swapping is immediate. New requests instantly get the updated service. Existing scopes safely finish using the old service.
- 👎 Cons: Multiple instances of the service can temporarily co-exist in memory during the transition.
2. Blocking
- Incoming service scope requests are paused/blocked.
- The engine waits for all active service scopes using the old instance to finish.
- The old service is disposed.
- A new service instance is initialized and unblocks incoming requests.
- 👍 Pros: Guarantees that exactly one instance of the service is alive at any given moment.
- 👎 Cons: Slow-running scopes will stall new requests, causing latency or timeouts in downstream systems like APIs.
🛠️ Installation & Registration
IServiceCollection Registration
Registration follows standard .NET patterns and fully supports standard, named, and keyed options/services.
// Program.cs
// 1. Unnamed options instance
builder.Services.Configure<MyOptions>(builder.Configuration.GetSection("MyOptions"));
builder.Services.AddConfigurationScoped<MyOptions, MyService>((sp, options) => new MyService(options));
// 2. Named options instance
builder.Services.Configure<MyOptions>("Options1", builder.Configuration.GetSection("MyOptions"));
builder.Services.AddConfigurationScoped<MyOptions, MyService>("Options1", (sp, options) => new MyService(options));
// 3. Keyed service registration
builder.Services.Configure<MyOptions>("Options1", builder.Configuration.GetSection("MyOptions1"));
builder.Services.Configure<MyOptions>("Options2", builder.Configuration.GetSection("MyOptions2"));
builder.Services.AddKeyedConfigurationScoped<MyOptions, MyService>("Options1", "ServiceKey1", (sp, key, options) => new MyService(options));
builder.Services.AddKeyedConfigurationScoped<MyOptions, MyService>("Options2", "ServiceKey2", (sp, key, options) => new MyService(options));
💻 Usage Examples
1. In Controllers
The service type is registered as scoped internally. It resolves automatically within an active HTTP request scope. Reference counts increment when the request starts and decrement when the request ends.
// MyServiceController.cs
[ApiController]
[Route("[controller]")]
public class MyServiceController : ControllerBase
{
private readonly MyService _myService;
// Resolved automatically behind the scenes via IConfigurationScopedServiceScopeFactory
public MyServiceController(MyService myService)
{
_myService = myService;
}
}
2. In Minimal APIs
Usage is identical and fully compatible with Minimal API parameter binding.
app.MapGet("/test", ([FromServices] MyService myService) =>
{
// Use myService safely here
return Results.Ok();
});
3. Outside of Request Scopes (e.g., Background Tasks)
To resolve ConfigurationScoped services in background workers, use IConfigurationScopedServiceScopeFactory<TServiceType> to safely manage the reference lifespan manually.
// MyBackgroundService.cs
public class MyBackgroundService : BackgroundService
{
private readonly IConfigurationScopedServiceScopeFactory<MyService> _myServiceScopeFactory;
public MyBackgroundService(IConfigurationScopedServiceScopeFactory<MyService> myServiceScopeFactory)
{
_myServiceScopeFactory = myServiceScopeFactory;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
// Scope creation increments the reference count
using (var scope = _myServiceScopeFactory.Create())
{
var service = scope.Service;
// Execute logic with the service instance safely
}
// Disposing the scope decrements the reference count
await Task.Delay(1000, stoppingToken);
}
}
}
⚠️ Why is IOptionsMonitor Potentially Unsafe As a Reload Trigger?
IOptionsMonitor change notifications are entirely disconnected from the .NET request pipeline. Updates execute on a background thread asynchronously, leaving application code vulnerable to race conditions and mid-request state modifications.
The Race Condition Problem
Consider this common but flawed implementation:
// MyService.cs
public class MyService
{
private MyOptions _options;
public MyService(IOptionsMonitor<MyOptions> optionsMonitor)
{
_options = optionsMonitor.CurrentValue;
optionsMonitor.OnChange(o => _options = o); // Updates via background thread
}
public bool IsFeatureAEnabled() => _options.FeatureAEnabled;
public int DoFeatureA()
{
if (!_options.FeatureAEnabled)
{
throw new Exception("Feature A is disabled!");
}
return 1;
}
}
// MyServiceController.cs
[HttpGet]
public int Get()
{
if (_myService.IsFeatureAEnabled())
{
// 💥 RACE CONDITION: If appsettings.json is saved and modifies MyOptions
// right here, the next line throws an unexpected Exception!
return _myService.DoFeatureA();
}
return -1;
}
The controller logic looks safe on paper: it checks if the feature is active before execution. However, if a configuration change occurs exactly between the check and the method invocation, your logic flow is now in an invalid state: It would have never reached that line of code if the initial conditions of the request were held constant.
How to reproduce this issue in the repository:
- Run the
IOptionsMonitorSampleproject and open Swagger (https://localhost:7028/swagger). - Open
appsettings.jsonand get ready to make a change to theFeatureAEnabledsetting. - IN swagger, execute the
TestWithDelayendpoint (introduces a 10-second delay between the feature check and execution to give you time to make the settings change). - In
appsettings.jsonchange"FeatureAEnabled"tofalse, then save. - The request will throw an exception.
ConfigurationScopedService prevents this structural "rug pull" entirely, ensuring your application stays predictable and reliable.
| 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 was computed. 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 was computed. 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 was computed. |
| .NET Framework | net461 was computed. net462 was computed. 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. |
-
.NETStandard 2.0
- Microsoft.AspNetCore.Http.Abstractions (>= 2.3.11)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.9)
- Microsoft.Extensions.Options (>= 10.0.9)
- Nito.AsyncEx.Coordination (>= 5.1.2)
- Nito.Disposables (>= 2.5.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Updates nugets and namespace