Mock IConfigurationSection to return array of string - c#

Using this answer to mock the IConfiguration methods from an ASP.NET Core app.
I need to mock an IConfigurationSection to return a string array.
My configuration class looks like this:
public class LoggingConfiguration
{
public string ApplicationName { get; set; }
public string[] Loggers { get; set; }
}
appsettings.json
{
"Logging": {
"LoggingConfiguration": {
"ApplicationName": "Some app",
"Loggers": [ "DiskLogger", "MemoryLogger"],
"DiskLogger": {
"SomeSettingOne" : "setting",
"SomeSettingTwo" : "setting",
},
"MemoryLogger": {
"AnotherSetting": "...",
}
}
}
In setting up the mocks - I have two problems.
I can't figure out how to mock an IConfigurationSection that would return string[] (loggers)
I am getting an exception when I try to setup the GetChildren() method on the LoggingSectionMock
public void Setup()
{
var applicationNameConfigurationSectionMock = new Mock<IConfigurationSection>();
applicationNameConfigurationSectionMock.Setup(m => m.Value).Returns(TestingApplicationName);
var loggerNamesConfigurationSectionMock = new Mock<IConfigurationSection>();
loggerNamesConfigurationSectionMock.Setup(m => m.GetChildren()).Returns(GetLoggerNamesSection);
//Throwing Method Not Found exception
LoggingSectionMock.Setup(m => m.GetChildren()).Returns(new List<IConfigurationSection>
{applicationNameConfigurationSectionMock.Object, loggerNamesConfigurationSectionMock.Object});
ConfigurationMock.Setup(m => m.GetSection($"{Logging}:{LoggingConfiguration}"))
.Returns(() => LoggingSectionMock.Object);
}
private IEnumerable<IConfigurationSection> GetLoggerNamesSection()
{
var loggerNamesConfigurationSections = new List<IConfigurationSection>();
LoggerNames.ToList().ForEach(loggerName =>
{
var configSectionMock = new Mock<IConfigurationSection>();
configSectionMock.Setup(m => m.Value).Returns(loggerName);
loggerNamesConfigurationSections.Add(configSectionMock.Object);
});
return loggerNamesConfigurationSections;
}

As an alternative you can take advantage of ConfigurationBuilder's AddInMemoryCollection:
Reference Memory configuration provider
Setup
IConfiguration configRoot = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string>
{
{ "ApplicationName", "App" },
{ "Loggers:0", "1" },
{ "Loggers:1", "2" },
{ "Loggers:2", "3" }
})
.Build();
Usage
LoggingConfiguration config = configRoot.Get<LoggingConfiguration>();

Related

Cleaning up startup.cs with extension methods?

I configure my database context by allowing changing of the connectionString based on the users cookie:
services.AddDbContext<HotdogContext>((serviceProvider, dbContextBuilder) =>
{
string connectionString = "DatabaseUS"; // Default connection string
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
var dbNameCookie = httpContextAccessor.HttpContext.Request.Cookies.ContainsKey("country");
if (dbNameCookie) // debug
{
Console.Write("Country cookie exists!");
}
var country = httpContextAccessor.HttpContext.Request.Cookies["country"]?.ToString() ?? "null";
if (country.Equals("null"))
{
Console.WriteLine("Country cookie is null");
}
else if (country.Equals("USA"))
{
Console.WriteLine("Set USA connection string");
connectionString = "DatabaseUS";
}
else if (country.Equals("AUS"))
{
connectionString = "DatabaseAUS";
Console.WriteLine("Set Australia connection string");
}
else if (country.Equals("S.A."))
{
//connectionString = "DatabaseSA";
Console.WriteLine("Set South America connection string");
}
dbContextBuilder.UseSqlServer(Configuration.GetConnectionString(connectionString));
});
This clutters the startup.cs if I add additional databaseContexts to the program. Is there a way I can hide this logic from startup.cs and use extension methods?
services.AddDbContext<HotdogContext>((serviceProvider, dbContextBuilder) =>
{
string connectionString = "DatabaseUS"; // Default connection string
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
var country = dbContextBuilder.GetRequestedCountry(httpContextAccessor); // Extension method
if(country !=null) connectionString = country;
dbContextBuilder.UseSqlServer(Configuration.GetConnectionString(connectionString));
});
One way is to define a service interface, then implement with your logic:
public interface IConnectionStringNameService
{
string GetName();
}
public class ConnectionStringNameFromCookieService
: IConnectionStringNameService
{
private readonly HttpContext _httpContext;
public ConnectionStringNameFromCookieService(
HttpContextAccessor httpContextAccessor )
{
_httpContext = httpContextAccessor.HttpContext;
}
public string GetName()
{
var country = _httpContext.Request
.Cookies[ "country" ]
?? "default value"; // or throw exception
return country switch =>
{
"USA" => "DatabaseUS",
"AUS" => "DatabaseAUS",
"S.A." => "DatabaseSA",
_ => "Default name or throw",
}
}
}
Then you register the service implementation with the container:
services.AddScoped<IConnectionStringNameService, ConnectionStringNameFromCookieService>();
Then you can resolve this service from the service provider when you need need the connection string name:
var connStrNameSvc = serviceProvider.GetRequiredService<IConnectionStringNameService>.();
var connStrName = connStrNameSvc.GetName();
In place (in the startup file), the above would abstract to:
services.AddDbContext<HotdogContext>(
(serviceProvider, dbContextBuilder) =>
{
var connStrName = serviceProvider
.GetRequiredService<IConnectionStringNameService>()
.GetName();
dbContextBuilder.UseSqlServer(
Configuration.GetConnectionString( connStrName ) );
});
But you can then use an extension method to abstract this call to a single, simple line:
public class BlahBlahExtensionMethods
{
// come up with a better name
public IServiceCollection AddDbContextUsingCookieForConnStrName<TDbContext>(
this IServiceCollection services )
where TDbContext : DbContext
{
return services.AddDbContext<TDbContext>(
( serviceProvider, dbContextBuilder ) =>
{
var connStrName = serviceProvider
.GetRequiredService<IConnectionStringNameService>()
.GetName();
dbContextBuilder.UseSqlServer(
Configuration.GetConnectionString( connStrName ) );
});
}
}
Then in startup you'd only need:
services.AddDbContextUsingCookieForConnStrName<HotdogContext>();
Or you can inject the name provider into HotdogContext and use it in an OnConfiguring( DbContextOptionsBuilder ) method override.

How can I populated an IEnumerable<MyType>() on an object via configuration?

I'm trying to populate the following object using the Option pattern:
public class MyParentPolicyOptions
{
public IEnumerable<SomePolicySettings> SomePolicySettings { get; set; }
}
My config json looks like this:
{
"MyParentPolicy": {
"SomePolicies": [
{
"Name": "Default",
"SomeSetting": 3,
"IsHappy": false
},
{
"Name": "Custom",
"SomeSetting": 5,
"IsHappy": true
}
]
}
For the configuration I do something like this:
serviceConfigurationDefinition.Optional("MyParentPolicy", Validators.MyParentPolicyOptions());
At the point of building the configuration builder I can see it has my properties as expected in the following pattern:
{[MyParentPolicy:SomePolicies:0:Name, Default}]
{[MyParentPolicy:SomePolicies:0:SomeSetting, 3}]
{[MyParentPolicy:SomePolicies:0:IsHappy, false}]
However, after applying this configuration root to the ServiceConfigurationDefinition my actual MyParentPolicyOptions.SomePolicySettings is still null. It seems to work for other strongly typed objects but I can't get it to work for Lists / IEnumerables / arrays etc.
Just to add, I've just tried this with Dictionary<int, SomePolicySetting> in the hope that the automatic indexing would mean this was actually a dictionary type, but didn't work.
I don't know for the serviceConfigurationDefinition.Optional() method you use. In general I do it this way. You're right IEnumerable is working. The issue is somewhere else in your code. The following example is working.
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices( (context,services) =>
{
services.AddOptions();
services.Configure<MyParentPolicy>(context.Configuration.GetSection("MyParentPolicy"));
services.AddHostedService<Worker>();
})
.Build();
await host.RunAsync();
public class MyParentPolicy
{
public IEnumerable<SomePolicySettings> SomePolicies { get; set; }
}
public class SomePolicySettings
{
public string Name { get; set; }
public string SomeSetting { get; set; }
public bool IsHappy { get; set; }
}
and appsettings.json:
{
"MyParentPolicy": {
"SomePolicies": [
{
"Name": "Default",
"SomeSetting": 3,
"IsHappy": false
},
{
"Name": "Custom",
"SomeSetting": 5,
"IsHappy": true
}
]
}
}
And finally retrieve the options with IOptionsMonitor<MyParentPolicy> for example:
public Worker(ILogger<Worker> logger, IOptionsMonitor<MyParentPolicy> options)
{
_logger = logger;
_parentPolicyOptions = options.CurrentValue;
}
The easiest way is to get it from startup configuration
List<SomePolicySettings> settings= configuration.GetSection("MyParentPolicy")
.Get<MyParentPolicy>().SomePolicies.ToList();
if you want to use it everywhere inside of your app , you can create a service
startup
services.Configure<MyParentPolicy>(configuration.GetSection("MyParentPolicy"));
services.AddSingleton<SomePolicySettingsService>();
service class
public class SomePolicySettingsService
{
private readonly List<SomePolicySettings> _somePolicySettings;
public SomePolicySettingsService(IOptions<MyParentPolicy> myParentPolicy)
{
_somePolicySettings = myParentPolicy.Value.SomePolicies;
}
public SomePolicySettings GetDefaultPolicySettings()
{
return _somePolicySettings.FirstOrDefault(i=>i.Name=="Default");
}
public SomePolicySettings GetCustomPolicySettings()
{
return _somePolicySettings.FirstOrDefault(i => i.Name == "Custom");
}
}
or instead of creating and injecting service, you can just inject
" IOptions myParentPolicy" in the constructor everywhere you need.

How to Mock IConfiguration.GetValue<string>

I'm trying to mock the configuration, but urlVariable keeps returning null, I also could not mock GetValue since it's static extionsion under Configuration Builder
public static T GetValue<T>(this IConfiguration configuration, string key);
Here's what I tried so far
// Arrange
var mockIConfigurationSection = new Mock<IConfigurationSection>();
mockIConfigurationSection.Setup(x => x.Value).Returns("SomeUrl");
mockIConfigurationSection.Setup(x => x.Key).Returns("Url");
var configuration = new Mock<IConfiguration>();
configuration.Setup(c => c.GetSection(It.IsAny<String>())).Returns(mockIConfigurationSection.Object);
// Act
var result = target.Test();
The method
public async Task Test()
{
var urlVariable = this._configuration.GetValue<string>("Url");
}
trying to mock these from app settings
{
"profiles": {
"LocalDB": {
"environmentVariables": {
"Url" : "SomeUrl"
}
}
}
}
Maybe you are not instantiating target properly. This piece of code should work.
void Main()
{
// Arrange
var mockIConfigurationSection = new Mock<IConfigurationSection>();
mockIConfigurationSection.Setup(x => x.Value).Returns("SomeUrl");
mockIConfigurationSection.Setup(x => x.Key).Returns("Url");
var configuration = new Mock<IConfiguration>();
configuration.Setup(c => c.GetSection(It.IsAny<String>())).Returns(mockIConfigurationSection.Object);
var target = new TestClass(configuration.Object);
// Act
var result = target.Test();
//Assert
Assert.Equal("SomeUrl", result);
}
public class TestClass
{
private readonly IConfiguration _configuration;
public TestClass(IConfiguration configuration) { this._configuration = configuration; }
public string Test()
{
return _configuration.GetValue<string>("Url");
}
}
Also, you might want to explore OptionsPattern
You don't need to mock something which can be set created manually.
Use ConfigurationBuilder to setup expected values.
[Fact]
public void TestConfiguration()
{
var value = new KeyValuePair<string, string>(
"profiles:LocalDb:environmentVariable:Url",
"http://some.url"
);
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new[] { value })
.Build();
var actual =
configuration.GetValue<string>("profiles:LocalDb:environmentVariable:Url");
actual.Should().Be("http://some.url");
}

Dependency Injection - directory inject one class in another class

I have a MailRepository class with the following structire:
public class MailRepository : IMailRepository
{
public MailRepository()
{
}
public async Task SendMail(string subject, string content, string recipientAddress)
{
}
}
I also have a LocalizationReposiory class with the following code:
public class LocalizationRepository : ILocalizationRepository
{
private readonly IStringLocalizer<LocalizationRepository> _localizer = null;
public LocalizationRepository(IStringLocalizer<LocalizationRepository> localizer)
{
_localizer = localizer;
}
public string TranslateSetting(string settingName, params string[] additionalParams)
{
return _localizer.GetString(settingName, additionalParams);
}
}
This is how I call SendMail method in MailRepository from a class:
var subject = _localizationRepository.TranslateSetting("Subject");
var content = _localizationRepository.TranslateSetting("Body");
await _mailRepository.SendMail(subject, content, "xyz#yahoo.com");
This is how dependency injection in startup looks like:
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton<IMailRepository>(services => new MailRepository());
builder.Services.AddLocalization(opts => { opts.ResourcesPath = "Resources"; });
builder.Services.Configure<RequestLocalizationOptions>(opts =>
{
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US")
};
opts.DefaultRequestCulture = new RequestCulture("en-US");
opts.SupportedCultures = supportedCultures;
opts.SupportedUICultures = supportedCultures;
});
builder.Services.AddSingleton<ILocalizationRepository, LocalizationRepository>();
}
}
Is there a way to inject the LocalizationRepository directly into MailRepository in order to avoid duplicating the following lines of code in multiple classes?
var subject = _localizationRepository.TranslateSetting("Subject");
var content = _localizationRepository.TranslateSetting("Body");
await _mailRepository.SendMail(subject, c,ontent "xyz#yahoo.com");
Inject ILocalizationRepository into MailRepository
public MailRepository(ILocalizationRepository localizationRepo)
{
//set private var
}
and then change how you register it in service provider
builder.Services.AddSingleton<IMailRepository, MailRepository>();

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);
}
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));
}
}
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);
});
}
}
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"));
...
}
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");
}
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();
}
Result
First
After change:

Categories

Resources