I am using the IOptions
pattern as described in the official documentation.
This works fine when I am reading values from appsetting.json
, but how do I update values and save changes back to appsetting.json
?
In my case, I have a few fields that can be edited from the user interface (by admin user in application). Hence I am looking for the ideal approach to update these values via the option accessor.
Add Json File After adding the file, right click on appsettings. json and select properties. Then set “Copy to Ouptut Directory” option to Copy Always. Add few settings to json file, so that you can verify that those settings are loaded.
The appsettings. json file is generally used to store the application configuration settings such as database connection strings, any application scope global variables, and much other information.
In order to add AppSettings. json file, right click on the Project in Solution Explorer. Then click Add, then New Item and then choose App Settings File option (shown below) and click Add button. Once the File is created, it will have a DefaultConnection, below that a new AppSettings entry is added.
To define the connection strings in appsettings. json it is important to specify it in the right section of the JSON structure. Now we can read it in our code by calling the GetConnectionString method in the Microsoft. Extensions.
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 = "..."; });
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With