Reputation: 333
So I have a .NET Core 2.2 app running on an Azure VM with Windows Server 2019 which has the following disk configuration:
The disk on the red box is where the App files are located. When the configuration file is updated either programatically or manually, IOptionsMonitor<T>
is not picking up the changes.
As stated in this link:
As mentioned in the documentation, just enabling
reloadOnChange
and then injectingIOptionsSnapshot<T>
instead ofIOptions<T>
will be enough. That requires you to have properly configured that typeT
though.
Which I did, as shown in this code:
private IConfiguration BuildConfig()
{
return new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("Config.json", false, reloadOnChange: true)
.Build();
}
public async Task MainAsync()
{
AppDomain.CurrentDomain.ProcessExit += ProcessExit;
...
IServiceCollection services = ConfigureServices();
// Configures the writable options from https://github.com/Nongzhsh/Awesome.Net.WritableOptions
services.ConfigureWritableOptions<ConfigurationSettings>(_config.GetSection("configurationSettings"), "ConfigDev.json");
// ConfigurationSettings is the POCO representing the config.json contents.
services.Configure<ConfigurationSettings>(_config.GetSection("configurationSettings"));
...
}
I haven't implemented the OnChange
method since I'm assuming that the values should be automatically updated once the file's contents have changed. I have also tried setting the .NET Core's DOTNET_USE_POLLING_FILE_WATCHER
to true
but it did not work.
Here's is my code for reading and writing values to the configuration file:
public TimeService(
IServiceProvider provider,
IWritableOptions<ConfigurationSettings> writeOnlyOptions,
IOptionsMonitor<ConfigurationSettings> hotOptions)
{
_provider = provider;
_writeOnlyOptions = writeOnlyOptions;
_hotOptions = hotOptions;
}
private async Task EnsurePostedGameSchedules()
{
DateTime currentTime = DateTime.Now;
...
# region [WINDOWS ONLY] Lines for debugging.
// _hotOptions is the depency-injected IOptionsMonitor<T> object.
if (ConnectionState == ConnectionState.Connected)
{
await debugChannel.SendMessageAsync(
embed: RichInfoHelper.CreateEmbed(
"What's on the inside?",
$"Connection State: {ConnectionState}{Environment.NewLine}" +
$"Last Message ID: {_hotOptions.CurrentValue.LatestScheduleMessageID}{Environment.NewLine}" +
$"Last Message Timestamp (Local): {new ConfigurationSettings { LatestScheduleMessageID = Convert.ToUInt64(_hotOptions.CurrentValue.LatestScheduleMessageID) }.GetTimestampFromLastScheduleMessageID(true)}{Environment.NewLine}" +
$"Current Timestamp: {DateTime.Now}",
"").Build());
}
#endregion
if (new ConfigurationSettings { LatestScheduleMessageID = _hotOptions.CurrentValue.LatestScheduleMessageID }.GetTimestampFromLastScheduleMessageID(true).Date != currentTime.Date &&
currentTime.Hour >= 1)
{
...
try
{
...
if (gameScheds?.Count > 0)
{
if (gameSchedulesChannel != null)
{
// The line below updates the configuration file.
_writeOnlyOptions.Update(option =>
{
option.LatestScheduleMessageID = message?.Id ?? default;
});
}
}
}
catch (Exception e)
{
Console.WriteLine(e.Message + Environment.NewLine + e.StackTrace);
}
}
}
And here's the config POCO:
public class ConfigurationSettings
{
public string Token { get; set; }
public string PreviousVersion { get; set; }
public string CurrentVersion { get; set; }
public Dictionary<string, ulong> Guilds { get; set; }
public Dictionary<string, ulong> Channels { get; set; }
public ulong LatestScheduleMessageID { get; set; }
public string ConfigurationDirectory { get; set; }
public DateTime GetTimestampFromLastScheduleMessageID(bool toLocalTime = false) =>
toLocalTime ?
new DateTime(1970, 1, 1).AddMilliseconds((LatestScheduleMessageID >> 22) + 1420070400000).ToLocalTime() :
new DateTime(1970, 1, 1).AddMilliseconds((LatestScheduleMessageID >> 22) + 1420070400000);
}
Is there anything that I still need to do in order for IOptionsMonitor<T>
to pick up the config changes in the config file?
EDIT: I forgot to tell how I configured the entire app. The program by the way is a long-running .NET Core console app (not a web app) so this is how the entire program is configured:
using ...
namespace MyProject
{
public class Program
{
static void Main(string[] args) => new Program().MainAsync().GetAwaiter().GetResult();
variables...
public async Task MainAsync()
{
AppDomain.CurrentDomain.ProcessExit += ProcessExit;
_client = new DiscordSocketClient();
_config = BuildConfig();
IServiceCollection services = ConfigureServices();
services.ConfigureWritableOptions<ConfigurationSettings>(_config.GetSection("configurationSettings"), "Config.json");
services.Configure<ConfigurationSettings>(_config.GetSection("configurationSettings"));
IServiceProvider serviceProvider = ConfigureServiceProvider(services);
serviceProvider.GetRequiredService<LogService>();
await serviceProvider.GetRequiredService<CommandHandlingService>().InitializeAsync(_config.GetSection("configurationSettings"));
serviceProvider.GetRequiredService<TimeService>().Initialize(_config.GetSection("configurationSettings"));
await _client.LoginAsync(TokenType.Bot, _config.GetSection("configurationSettings")["token"]);
await _client.StartAsync();
_client.Ready += async () =>
{
...
};
await Task.Delay(-1);
}
private void ProcessExit(object sender, EventArgs e)
{
try
{
...
}
catch (Exception ex)
{
...
}
}
private IServiceCollection ConfigureServices()
{
return new ServiceCollection()
// Base Services.
.AddSingleton(_client)
.AddSingleton<CommandService>()
// Logging.
.AddLogging()
.AddSingleton<LogService>()
// Extras. Is there anything wrong with this?
.AddSingleton(_config)
// Command Handlers.
.AddSingleton<CommandHandlingService>()
// Add additional services here.
.AddSingleton<TimeService>()
.AddSingleton<StartupService>()
.AddTransient<ConfigurationService>();
}
public IServiceProvider ConfigureServiceProvider(IServiceCollection services) => services.BuildServiceProvider();
private IConfiguration BuildConfig()
{
return new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("Config.json", false, true)
.Build();
}
}
}
Upvotes: 0
Views: 4780
Reputation: 333
It now worked without adding anything. I just let the app run using the compiled executable when I let my project target .NET Core 3.1. The app before was targeting .NET Core 2.2 and ran via PowerShell. I have no idea PowerShell has issues with IOptionsMonitor<T>
.
Upvotes: 2
Reputation: 23141
According to my test, if we want to use IOptionsMonitor<T>
to pick up the config changes in the config file, please refer to the following steps
My config.json
{
"configurationSettings": {
"Token": "...",
"PreviousVersion": "145.8.3",
"CurrentVersion": "145.23.4544",
"Guilds": {
"this setting": 4
},
"Channels": {
"announcements": 6
},
"LatestScheduleMessageID": 456,
"ConfigurationDirectory": "test"
}
}
My POCO
public class MyOptions
{
public string Token { get; set; }
public string PreviousVersion { get; set; }
public string CurrentVersion { get; set; }
public Dictionary<string, ulong> Guilds { get; set; }
public Dictionary<string, ulong> Channels { get; set; }
public ulong LatestScheduleMessageID { get; set; }
public string ConfigurationDirectory { get; set; }
public DateTime GetTimestampFromLastScheduleMessageID(bool toLocalTime = false) =>
toLocalTime ?
new DateTime(1970, 1, 1).AddMilliseconds((LatestScheduleMessageID >> 22) + 1420070400000).ToLocalTime() :
new DateTime(1970, 1, 1).AddMilliseconds((LatestScheduleMessageID >> 22) + 1420070400000);
}
public interface IWritableOptions<out T> : IOptions<T> where T : class, new()
{
void Update(Action<T> applyChanges);
}
public class WritableOptions<T> : IWritableOptions<T> where T : class, new()
{
private readonly IHostingEnvironment _environment;
private readonly IOptionsMonitor<T> _options;
private readonly string _section;
private readonly string _file;
public WritableOptions(
IHostingEnvironment environment,
IOptionsMonitor<T> options,
string section,
string file)
{
_environment = environment;
_options = options;
_section = section;
_file = file;
}
public T Value => _options.CurrentValue;
public T Get(string name) => _options.Get(name);
public void Update(Action<T> applyChanges)
{
var fileProvider = _environment.ContentRootFileProvider;
var fileInfo = fileProvider.GetFileInfo(_file);
var physicalPath = fileInfo.PhysicalPath;
var jObject = JsonConvert.DeserializeObject<JObject>(File.ReadAllText(physicalPath));
var sectionObject = jObject.TryGetValue(_section, out JToken section) ?
JsonConvert.DeserializeObject<T>(section.ToString()) : (Value ?? new T());
applyChanges(sectionObject);
jObject[_section] = JObject.Parse(JsonConvert.SerializeObject(sectionObject));
File.WriteAllText(physicalPath, JsonConvert.SerializeObject(jObject, Formatting.Indented));
}
}
public static class ServiceCollectionExtensions
{
public static void ConfigureWritable<T>(
this IServiceCollection services,
IConfigurationSection section,
string file = "appsettings.json") where T : class, new()
{
services.Configure<T>(section);
services.AddTransient<IWritableOptions<T>>(provider =>
{
var environment = provider.GetService<IHostingEnvironment>();
var options = provider.GetService<IOptionsMonitor<T>>();
return new WritableOptions<T>(environment, options, section.Key, file);
});
}
}
public void ConfigureServices(IServiceCollection services)
{
var configBuilder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("Config.json", optional: false, reloadOnChange:true);
var config = configBuilder.Build();
services.ConfigureWritable<MyOptions>(config.GetSection("configurationSettings"));
...
}
private readonly IWritableOptions<Locations> _writableLocations;
public OptionsController(IWritableOptions<Locations> writableLocations)
{
_writableLocations = writableLocations;
}
//Update LatestScheduleMessageID
public IActionResult Change(string value)
{
_writableLocations.Update(opt => {
opt.LatestScheduleMessageID = value;
});
return Ok("OK");
}
private readonly IOptionsMonitor<MyOptions> _options;
public HomeController(ILogger<HomeController> logger, IHostingEnvironment env, IOptionsMonitor<MyOptions> options)
{
_logger = logger;
_env = env;
_options = options;
}
public IActionResult Index()
{
var content= _env.ContentRootPath;
var web = _env.WebRootPath;
@ViewBag.Message = _options.CurrentValue.LatestScheduleMessageID;
return View();
}
Upvotes: 1