I’m using IOptionsMonitor where T is dependent on other options in my service. There seems to be a race condition when using the OnChange event because the IOptions are initialized asynchronously on Reload, leading to T being initialized before the dependent options. This causes old values to appear in the OnChange event.
To demonstrate the issue let’s have this sample repo:
public class FirstOptions
{
public string FirstValue { get; set; }
}
public class SecondOptions
{
public string SecondValue { get; set; }
}
internal class Worker : IHostedService
{
private readonly IDisposable _disposable;
private readonly string _filePath = "Config.ini";
public Worker(IOptionsMonitor<SecondOptions> options)
{
Console.WriteLine($"Ctor: "{options.CurrentValue.SecondValue}"");
_disposable = options.OnChange(OnOptionsChanged);
}
private void OnOptionsChanged(SecondOptions options, string? arg2)
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} OnChange: Second: {options.SecondValue}");
}
public async Task StartAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 20; i++)
{
await Task.Delay(2000);
WriteFirstValue(_filePath, i.ToString());
}
}
public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
private static void WriteFirstValue(string filePath, string value)
{
Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff} Property changed to " + value);
File.WriteAllText(filePath,
"[FirstSection]"
+ Environment.NewLine + $"FirstValue="{value}"");
}
}
internal class Program
{
private static void Main(string[] args)
{
IConfigurationRoot configurationRoot = new ConfigurationBuilder()
.SetBasePath(Environment.CurrentDirectory)
.AddIniFile("config.ini", false, true)
.Build();
var provider = configurationRoot.Providers.OfType<IniConfigurationProvider>().First();
IHostBuilder builder = Host.CreateDefaultBuilder(args);
builder.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
services.AddOptions<FirstOptions>().Bind(configurationRoot.GetSection("FirstSection"));
services.AddOptions<SecondOptions>()
.Bind(configurationRoot.GetSection("SecondSection"))
.Configure<IOptionsMonitor<FirstOptions>>((secondOpt, firstOpt) => secondOpt.SecondValue = firstOpt.CurrentValue.FirstValue);
//.Configure(secondOpt => secondOpt.SecondValue = configurationRoot.GetSection("FirstSection").GetValue<string>("FirstValue"));
});
IHost host = builder.Build();
host.Run();
}
}
The output shows a lag in updating SecondOptions values:
15:53:00.677 Property changed to 0
15:53:00.957 OnChange: Second: 0
15:53:01.222 OnChange: Second: 0
15:53:02.703 Property changed to 1
15:53:02.970 OnChange: Second: 0
15:53:03.222 OnChange: Second: 1
15:53:04.713 Property changed to 2
15:53:04.975 OnChange: Second: 1
15:53:05.228 OnChange: Second: 2
15:53:06.727 Property changed to 3
15:53:06.994 OnChange: Second: 2
15:53:07.259 OnChange: Second: 3
Using IConfigurationRoot directly solves the problem, but I’d prefer not to use it. According to ASP.NET documentation for 5.0, “Don’t use IOptions or IOptionsMonitor in Startup.ConfigureServices. An inconsistent options state may exist due to the ordering of service registrations.” I’m not sure if I’m facing the same issue. However, this comment is missing in the documentation for 6.0/8.0.
Is there an alternative way to fix this without using ConfigurationRoot?
3