I am using the IOptions
pattern as described in the official documentation.
This works fine when I am reading values from appsetting.json
,
Simplified version of Matze's answer:
public interface IWritableOptions<out T> : IOptionsSnapshot<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);
});
}
}
Usage:
services.ConfigureWritable<MyOptions>(Configuration.GetSection("MySection"));
Then:
private readonly IWritableOptions<MyOptions> _options;
public MyClass(IWritableOptions<MyOptions> options)
{
_options = options;
}
To save the changes to the file:
_options.Update(opt => {
opt.Field1 = "value1";
opt.Field2 = "value2";
});
And you can pass a custom json file as optional parameter (it will use appsettings.json by default):
services.ConfigureWritable<MyOptions>(Configuration.GetSection("MySection"), "appsettings.custom.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<T>
interface that extends IOptions<T>
; so I can just replace IOptions<T>
by IWritableOptions<T>
whenever I want to read and write settings.
public interface IWritableOptions<out T> : IOptions<T> where T : class, new()
{
void Update(Action<T> applyChanges);
}
Also, I came up with IOptionsWriter
, which is a component that is intended to be used by IWritableOptions<T>
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<JObject> 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<T> : IWritableOptions<T> where T : class, new()
{
private readonly string sectionName;
private readonly IOptionsWriter writer;
private readonly IOptionsMonitor<T> options;
public WritableOptions(
string sectionName,
IOptionsWriter writer,
IOptionsMonitor<T> options)
{
this.sectionName = sectionName;
this.writer = writer;
this.options = options;
}
public T Value => this.options.CurrentValue;
public void Update(Action<T> applyChanges)
{
this.writer.UpdateOptions(opt =>
{
JToken section;
T sectionObject = opt.TryGetValue(this.sectionName, out section) ?
JsonConvert.DeserializeObject<T>(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<T>(
this IServiceCollection services,
IConfigurationRoot configuration,
string sectionName,
string file) where T : class, new()
{
services.Configure<T>(configuration.GetSection(sectionName));
services.AddTransient<IWritableOptions<T>>(provider =>
{
var environment = provider.GetService<IHostingEnvironment>();
var options = provider.GetService<IOptionsMonitor<T>>();
IOptionsWriter writer = new OptionsWriter(environment, configuration, file);
return new WritableOptions<T>(sectionName, writer, options);
});
}
}
Which can be used in ConfigureServices
like...
services.ConfigureWritable<CustomizableOptions>(this.Configuration,
"MySection", "appsettings.custom.json");
In my Controller
class I can just demand an IWritableOptions<CustomizableOptions>
instance, that has the same characteristics as IOptions<T>
, but also allows to change and store configuration values.
private IWritableOptions<CustomizableOptions> options;
...
this.options.Update((opt) => {
opt.SampleOption = "...";
});
I hope that my scenario covers your intent, I wanted to override the appsettings.json values if there are environment variables passed to the app at startup.
I made use of the ConfigureOptions method that is available in dotnet core 2.1.
Here is the Model that is used for the JSON from appsettings.json
public class Integration
{
public string FOO_API {get;set;}
}
For the services in the statup.cs:
var section = Configuration.GetSection ("integration");
services.Configure<Integration> (section);
services.ConfigureOptions<ConfigureIntegrationSettings>();
Here is the implemenation:
public class ConfigureIntegrationSettings : IConfigureOptions<Integration>
{
public void Configure(Integration options)
{
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("FOO")))
options.FOO_API = Environment.GetEnvironmentVariable("FOO_API");
}
}
so if there is no value set it falls back to the appsettings.json
public static void SetAppSettingValue(string key, string value, string appSettingsJsonFilePath = null) {
if (appSettingsJsonFilePath == null) {
appSettingsJsonFilePath = System.IO.Path.Combine(System.AppContext.BaseDirectory, "appsettings.json");
}
var json = System.IO.File.ReadAllText(appSettingsJsonFilePath);
dynamic jsonObj = Newtonsoft.Json.JsonConvert.DeserializeObject < Newtonsoft.Json.Linq.JObject > (json);
jsonObj[key] = value;
string output = Newtonsoft.Json.JsonConvert.SerializeObject(jsonObj, Newtonsoft.Json.Formatting.Indented);
System.IO.File.WriteAllText(appSettingsJsonFilePath, output);
}