简体   繁体   English

如何让 IOptionsMonitor<T> 从托管在 Azure Windows Server VM 上的正在运行的 .NET Core 2.2 应用程序获取最新配置值?

[英]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:所以我有一个 .NET Core 2.2 应用程序在带有 Windows Server 2019 的 Azure VM 上运行,它具有以下磁盘配置:

磁盘配置

The disk on the red box is where the App files are located.红框上的磁盘是 App 文件所在的位置。 When the configuration file is updated either programatically or manually, IOptionsMonitor<T> is not picking up the changes.以编程方式或手动方式更新配置文件时, IOptionsMonitor<T>不会获取更改。

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.如文档中所述,只需启用reloadOnChange然后注入IOptionsSnapshot<T>而不是IOptions<T>就足够了。 That requires you to have properly configured that type T though.不过,这需要您正确配置该类型T

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.我还没有实现OnChange方法,因为我假设一旦文件的内容发生变化,这些值应该自动更新。 I have also tried setting the .NET Core's DOTNET_USE_POLLING_FILE_WATCHER to true but it did not work.我也试过将 .NET Core 的DOTNET_USE_POLLING_FILE_WATCHER设置为true但它没有用。

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:这是配置 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?为了让IOptionsMonitor<T>获取配置文件中的配置更改,我还需要做什么吗?

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:顺便说一下,该程序是一个长时间运行的 .NET Core 控制台应用程序(不是 Web 应用程序),因此整个程序的配置方式如下:

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.当我让我的项目面向 .NET Core 3.1 时,我只是让应用程序使用编译的可执行文件运行。 The app before was targeting .NET Core 2.2 and ran via PowerShell.之前的应用程序面向 .NET Core 2.2 并通过 PowerShell 运行。 I have no idea PowerShell has issues with IOptionsMonitor<T> .我不知道 PowerShell 有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根据我的测试,如果我们想使用IOptionsMonitor<T>来获取配置文件中的配置更改,请参考以下步骤我的 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我的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 Defile 类以保存更改
 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实现了 ServiceCollectionExtensions 的扩展方法,允许您轻松配置可写选项
    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请在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更改 Json 值
 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读取 JSON 值
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:更改后: 在此处输入图片说明

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 .NET Core 2.2 - 无法从 Azure AppConfig 获取应用程序配置 - .NET Core 2.2 - Unable to get app configuration from Azure AppConfig ASP.NET Core - 如何注入 IOptionsMonitor<T[]> - ASP.NET Core - how to inject IOptionsMonitor<T[]> .NET Core 2.2:如何在 Azure 应用服务中从 Azure AD 自动填充用户列表? - .NET Core 2.2 : How to auto-populate list of users from Azure AD in Azure app service.? Sharepoint 在线/365 集成(上传文件)在 Azure 和 WebApi C#.Net Core 2.2 上托管的 React 应用程序上 - Sharepoint online/365 integration (To upload files) on React app hosted on Azure and WebApi C# .Net Core 2.2 Azure上托管的ASP.NET Core 2.2 App Service返回500,没有引发异常 - ASP.NET Core 2.2 App Service hosted on Azure returning 500 without an exception being thrown 如何从托管在 **1&amp;1 Ionos** 托管服务器中的 ASP.NET Core 2.2 连接 MS SQL Server 数据库? - How to connect MS SQL Server database from ASP.NET Core 2.2 hosted in **1&1 Ionos** hosting server? 使用 .NET Core 2.2 从 Azure 存储中获取所有 Blob - Get all Blobs From Azure Storage using .NET Core 2.2 我如何从Windows服务器2016和.net .core 2.2上的Active Directory进行身份验证? - How would I authenticate from an Active Directory on a windows server 2016 and .net .core 2.2? 从 .NET 核心 2.2 迁移到 .NET 5 后,应用程序在部署到 azure 应用程序服务后无法启动 - 启动超时 - After migration from .NET core 2.2 to .NET 5 the application won't start after deploying to azure app service - startup timeout .net core 2.2 Windows服务完全启动后,如何配置托管服务才能开始? - How to configure hosted service to begin after .net core 2.2 windows service fully started?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM