I am using the IOptions pattern as described in the official documentation.
This works fine when I am reading values from appsetting.json,
At the time of writing this answer it seemed that there is no component provided by the Microsoft.Extensions.Options package that has functionality to write configuration values back to appsettings.json.
In one of my ASP.NET Core projects I wanted to enable the user to change some application settings - and those setting values should be stored in appsettings.json, more precisly in an optional appsettings.custom.json file, that gets added to the configuration if present.
Like this...
public Startup(IHostingEnvironment env)
{
IConfigurationBuilder builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile("appsettings.custom.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
this.Configuration = builder.Build();
}
I declared the IWritableOptions interface that extends IOptions; so I can just replace IOptions by IWritableOptions whenever I want to read and write settings.
public interface IWritableOptions : IOptions where T : class, new()
{
void Update(Action applyChanges);
}
Also, I came up with IOptionsWriter, which is a component that is intended to be used by IWritableOptions to update a configuration section. This is my implementation for the beforementioned interfaces...
class OptionsWriter : IOptionsWriter
{
private readonly IHostingEnvironment environment;
private readonly IConfigurationRoot configuration;
private readonly string file;
public OptionsWriter(
IHostingEnvironment environment,
IConfigurationRoot configuration,
string file)
{
this.environment = environment;
this.configuration = configuration;
this.file = file;
}
public void UpdateOptions(Action callback, bool reload = true)
{
IFileProvider fileProvider = this.environment.ContentRootFileProvider;
IFileInfo fi = fileProvider.GetFileInfo(this.file);
JObject config = fileProvider.ReadJsonFileAsObject(fi);
callback(config);
using (var stream = File.OpenWrite(fi.PhysicalPath))
{
stream.SetLength(0);
config.WriteTo(stream);
}
this.configuration.Reload();
}
}
Since the writer is not aware about the file structure, I decided to handle sections as JObject objects. The accessor tries to find the requested section and deserializes it to an instance of T, uses the current value (if not found), or just creates a new instance of T, if the current value is null. This holder object is than passed to the caller, who will apply the changes to it. Than the changed object gets converted back to a JToken instance that is going to replace the section...
class WritableOptions : IWritableOptions where T : class, new()
{
private readonly string sectionName;
private readonly IOptionsWriter writer;
private readonly IOptionsMonitor options;
public WritableOptions(
string sectionName,
IOptionsWriter writer,
IOptionsMonitor options)
{
this.sectionName = sectionName;
this.writer = writer;
this.options = options;
}
public T Value => this.options.CurrentValue;
public void Update(Action applyChanges)
{
this.writer.UpdateOptions(opt =>
{
JToken section;
T sectionObject = opt.TryGetValue(this.sectionName, out section) ?
JsonConvert.DeserializeObject(section.ToString()) :
this.options.CurrentValue ?? new T();
applyChanges(sectionObject);
string json = JsonConvert.SerializeObject(sectionObject);
opt[this.sectionName] = JObject.Parse(json);
});
}
}
Finally, I implemented an extension method for IServicesCollection allowing me to easily configure a writable options accessor...
static class ServicesCollectionExtensions
{
public static void ConfigureWritable(
this IServiceCollection services,
IConfigurationRoot configuration,
string sectionName,
string file) where T : class, new()
{
services.Configure(configuration.GetSection(sectionName));
services.AddTransient>(provider =>
{
var environment = provider.GetService();
var options = provider.GetService>();
IOptionsWriter writer = new OptionsWriter(environment, configuration, file);
return new WritableOptions(sectionName, writer, options);
});
}
}
Which can be used in ConfigureServices like...
services.ConfigureWritable(this.Configuration,
"MySection", "appsettings.custom.json");
In my Controller class I can just demand an IWritableOptions instance, that has the same characteristics as IOptions, but also allows to change and store configuration values.
private IWritableOptions options;
...
this.options.Update((opt) => {
opt.SampleOption = "...";
});