简体   繁体   中英

How to let IOptionsMonitor<T> get the latest configuration value from a running .NET Core 2.2 app hosted on an Azure Windows Server VM?

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 injecting IOptionsSnapshot<T> instead of IOptions<T> will be enough. That requires you to have properly configured that type T 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();
        }
}

}

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

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);
    }
  1. Defile a class to save changes
 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));
        }
    }
  1. Implemented an extension method for ServiceCollectionExtensions allowing you to easily configure a writable options
    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);
            });
        }
    }
  1. Please add the following code in Startup.cs
 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"));

            ...
        }

  1. Change the Json vaule
 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");
        }
  1. Read the JSON value
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();
        }
  1. Result

First在此处输入图片说明

After change: 在此处输入图片说明

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM