56

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.

Carsten
  • 11,287
  • 7
  • 39
  • 62
439
  • 720
  • 2
  • 7
  • 14
  • 1
    The framework provides a common infrastructure for reading configuration values, not to modify them. When it comes to modifying, you'll have to use the specific configuration provider to access and modify the underlying configuration source. – haim770 Dec 05 '16 at 09:32
  • 2
    "specific configuration provider to access and modify the underlying configuration source"? Could you please give me some reference to start with? – 439 Dec 05 '16 at 09:51
  • What is the configuration source you're intending to modify? – haim770 Dec 05 '16 at 09:52
  • Like I said in my post - appsetting.json, In this I have few application wide settings stored which I intend to modify from UI. – 439 Dec 05 '16 at 10:00
  • Is it an MVC application? – radu-matei Dec 05 '16 at 13:56
  • 1
    yes...it is ASP.NET Core MVC application. – 439 Dec 06 '16 at 03:16

8 Answers8

61

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 = "...";
});
Matze
  • 5,100
  • 6
  • 46
  • 69
  • Thank you very much. I have a follow up question regarding [`IConfigurationRoot`](https://stackoverflow.com/questions/48939567/how-to-access-iconfigurationroot-in-startup-on-net-core-2), maybe you know the answer :) – Christian Gollhardt Feb 23 '18 at 01:01
  • 2
    Is this still the only way, one year later? – Javier García Manzano May 23 '18 at 15:25
  • 1
    @JavierGarcíaManzano I think so; there´s an issue on GitHub (filed in March 2018), but it has been closed: https://github.com/aspnet/Home/issues/2973 – Matze May 28 '18 at 11:46
  • 2
    So before I go down this path, I just wanted to check... Almost 4 years later and we still have to do something like this? Is there not something baked in by now? What are people using to write to custom JSON option/config files in 2021? – Arvo Bowen Jan 19 '21 at 01:12
  • @ArvoBowen I think those people implement their own persisters that suit their needs. IMHO there is no generic solution that perfectly unbinds configuration and routes changes back to its source(s). If you only work with JSON-files you can go with the suggested solution, but if you combine several sources (maybe involving user-secrets, or environment variables), you may consider another solution to not put security at risk. – Matze Jan 19 '21 at 09:49
  • 2
    Thanks, @Matze. I don't mind making a new class to handle all my settings in general. I tend to go out of my way to do stuff like that usually... Only to then later find out there is a much easier way to get the same task done. Trying to evolve my coding practice and look for something already built first. I was looking at Derrell's NuGet package in the comments on this question (https://stackoverflow.com/a/45986656/1039753). It seems after all the work he put into it, he decided, in the end, doing that with JSON was too unstable I guess. So it gave me pause to ask the question. – Arvo Bowen Jan 19 '21 at 13:02
55

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");
ceferrari
  • 1,597
  • 1
  • 20
  • 25
  • 2
    I was a bit unsure of "MyOptions", but reading this helped me figure it out. https://codingblast.com/asp-net-core-configuration-reloading-binding-injecting/ – JsAndDotNet Sep 20 '18 at 08:18
  • 1
    Have created a github repo and pushed as nuget package - with some changes for further flexibility: https://github.com/dazinator/Dazinator.Extensions.WritableOptions – Darrell Jul 25 '19 at 11:51
  • 1
    Exellect solution. But while using it my code I encountered an error because of line: `JObject.Parse(JsonConvert.SerializeObject(sectionObject))`. This line will fail if sectionObject is an array. It can be replaced with: `JToken.Parse(JsonConvert.SerializeObject(sectionObject))` – Ajeet Singh Sep 21 '19 at 20:31
  • 1
    This solution is great but I have one problem. My updated file is not injected after redirection (old configuration is injected). I've created question with explanation https://stackoverflow.com/q/59677486/9684060 – Josef Trejbal Jan 10 '20 at 07:58
  • Should IWritableOptions be a singleton instead of a transient? – Gustavo Mauricio De Barros Apr 04 '23 at 15:57
10

I see a lot of answers use Newtonsoft.Json package to update appsettings. I will provide some some solutions that use System.Text.Json package (built-in on .Net Core 3 and above).

OPTION 1

Before you start updating appsettings.json file dynamically, ask yourself a question, how comlex is that part of appsettings.json that needs to be updated. If the part that needs to be updated is not very complex, you can use appsettings transformation functionality just for that part that that needs to be updated. Here's an example: Let's say my appsettings.json file looks like that:

{
    "Username": "Bro300",
    "Job": {
        "Title": "Programmer",
        "Type": "IT"
    }
}

And let's say I need to update only Job section. Instead of updating appsettings.json directly I can create a smaller file appsettings.MyOverrides.json that will look like this:

{
  "Job": {
    "Title": "Farmer",
    "Type": "Agriculture"
  }
}

And then make sure that this new file is added in my .Net Core app, and .Net Core will figure out how to load the new updated settings. Now the next step is to create a wrapper class that will hold values from appsettings.MyOverrides.json like this:

public class OverridableSettings
{
    public JobSettings Job { get; set; }
}

public class JobSettings
{
    public string Title { get; set; }
    public string Type { get; set; }
}

And then I can create my updater class that will look like this (notice that it takes in OverridableSettings and completely overrides appsettings.MyOverrides.json file:

public class AppSettingsUpdater
{
    public void UpdateSettings(OverridableSettings settings)
    {
        // instead of updating appsettings.json file directly I will just write the part I need to update to appsettings.MyOverrides.json
        // .Net Core in turn will read my overrides from appsettings.MyOverrides.json file
        const string SettinsgOverridesFileName = "appsettings.MyOverrides.json";
        var newConfig = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
        File.WriteAllText(SettinsgOverridesFileName, newConfig);
    }
}

Finally this is the code that demonstrates how to use it:

public static class Program
{
    public static void Main()
    {
        // Notice that appsettings.MyOverrides.json will contain only the part that we need to update, other settings will live in appsettings.json
        // Also appsettings.MyOverrides.json is optional so if it doesn't exist at the program start it's not a problem
        var configuration = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json")
            .AddJsonFile("appsettings.MyOverrides.json", optional: true)
            .Build();

        // Here we read our current settings
        var settings = configuration.Get<OverridableSettings>();

        var settingsUpdater = new AppSettingsObjectUpdater();
        settings.Job.Title = "Farmer";
        settings.Job.Type = "Agriculture";
        settingsUpdater.UpdateSettings(settings);

        // Here we reload the settings so the new values from appsettings.MyOverrides.json will be read
        configuration.Reload(); 
        // and here we retrieve the new updated settings
        var newJobSettings = configuration.GetSection("Job").Get<JobSettings>();
    }
}

OPTION 2

If the appsetting transformation does not fit you case, and you have to update values only one level deep, you can use this simple implementation:

public void UpdateAppSetting(string key, string value)
{
    var configJson = File.ReadAllText("appsettings.json");
    var config = JsonSerializer.Deserialize<Dictionary<string, object>>(configJson);
    config[key] = value;
    var updatedConfigJson = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true });
    File.WriteAllText("appsettings.json", updatedConfigJson);
}

OPTION 3

Finally, if you have some complex case and you need to update appsettings, multiple levels deep, here is another implementation, that expands on the previous option, and uses recursion to update the settings at any level:

public class AppSettingsUpdater
{
    private const string EmptyJson = "{}";
    public void UpdateAppSetting(string key, object value)
    {
        // Empty keys "" are allowed in json by the way
        if (key == null)
        {
            throw new ArgumentException("Json property key cannot be null", nameof(key));
        }

        const string settinsgFileName = "appsettings.json";
        // We will create a new file if appsettings.json doesn't exist or was deleted
        if (!File.Exists(settinsgFileName))
        {
            File.WriteAllText(settinsgFileName, EmptyJson);
        }
        var config = File.ReadAllText(settinsgFileName);

        var updatedConfigDict = UpdateJson(key, value, config);
        // After receiving the dictionary with updated key value pair, we serialize it back into json.
        var updatedJson = JsonSerializer.Serialize(updatedConfigDict, new JsonSerializerOptions { WriteIndented = true });

        File.WriteAllText(settinsgFileName, updatedJson);
    }

    // This method will recursively read json segments separated by semicolon (firstObject:nestedObject:someProperty)
    // until it reaches the desired property that needs to be updated,
    // it will update the property and return json document represented by dictonary of dictionaries of dictionaries and so on.
    // This dictionary structure can be easily serialized back into json
    private Dictionary<string, object> UpdateJson(string key, object value, string jsonSegment)
    {
        const char keySeparator = ':';

        var config = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonSegment);
        var keyParts = key.Split(keySeparator);
        var isKeyNested = keyParts.Length > 1;
        if (isKeyNested)
        {
            var firstKeyPart = keyParts[0];
            var remainingKey = string.Join(keySeparator, keyParts.Skip(1));

            // If the key does not exist already, we will create a new key and append it to the json
            var newJsonSegment = config.ContainsKey(firstKeyPart) && config[firstKeyPart] != null
                ? config[firstKeyPart].ToString()
                : EmptyJson;
            config[firstKeyPart] = UpdateJson(remainingKey, value, newJsonSegment);
        }
        else
        {
            config[key] = value;
        }
        return config;
    }
}

You can use, like this:

var settingsUpdater = new AppSettingsUpdater();
settingsUpdater.UpdateAppSetting("OuterProperty:NestedProperty:PropertyToUpdate", "new value");
Mykhailo Seniutovych
  • 3,527
  • 4
  • 28
  • 50
8
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);
}
Alper Ebicoglu
  • 8,884
  • 1
  • 49
  • 55
  • appsettings.json does not have to be valid json. For example in appsettings.json comments are allowed. Also dynamic ist extremly slow. – Michael Santos Sep 27 '21 at 11:37
3

While there is still not a way via the Options accessor, I'd like to preset a .NET 6 class that makes it quite easy to write back to the file. You can use the JsonNode class in the System.Text.Json.Nodes class. I'm using it to write back an encrypted connection string after reading a plain text one from appsettings.json.

There are examples of using Newtonsoft.Json.JsonConvert.DeserializeObject and deserializing into a dynamic type like @Alper suggested - but System.Text.Json could not do that. Well, now you sort of can :) (though not with a dynamic type).

In my example below, I tried to be minimalistic and simple. I used JsonNode to retrieve the value instead of a Dependency Injected IConfiguration. In a real web application, I'd be using the DI method. It really doesn't matter how you retrieve the setting, writing it back still means reconstructing the Json and updating the file on disk.

MS Link for JsonNode: https://learn.microsoft.com/en-us/dotnet/api/system.text.json.nodes.jsonnode?view=net-6.0

My appsettings.json sample:

{
  "sampleSection": {
    "someStringSetting": "Value One",
    "deeperValues": {
      "someIntSetting": 23,
      "someBooleanSetting": true
    }
  }
}

C# .NET 6 console application:

using System.Text.Json;
using System.Text.Json.Nodes;

const string AppSettingsPath = @"<PathToYourAppSettings.JsonFile>>\appsettings.json";
string appSettingsJson = File.ReadAllText(AppSettingsPath);
var jsonNodeOptions = new JsonNodeOptions { PropertyNameCaseInsensitive = true };
var node = JsonNode.Parse(appSettingsJson, jsonNodeOptions);

var options = new JsonSerializerOptions { WriteIndented = true };
Console.WriteLine("===========  Before ============");
Console.WriteLine(node.ToJsonString(options));


// Now you have access to all the structure using node["blah"] syntax
var stringSetting = (string) node["sampleSection"]["someStringSetting"];
var intSetting = (int) node["sampleSection"]["deeperValues"]["someIntSetting"];
var booleanSetting = (bool) node["sampleSection"]["deeperValues"]["someBooleanSetting"];

Console.WriteLine($"stringSetting: {stringSetting}, intSetting: {intSetting}, booleanSetting: {booleanSetting}");

// Now write new values back 
node["sampleSection"]["someStringSetting"] = $"New setting at {DateTimeOffset.Now}";
node["sampleSection"]["deeperValues"]["someIntSetting"] = -6;
node["sampleSection"]["deeperValues"]["someBooleanSetting"] = false;

Console.WriteLine("===========  After ============");
Console.WriteLine(node.ToJsonString(options));

// Or, to actually write it to disk: 
// File.WriteAllText(AppSettingsPath, node.ToJsonString(options));
Blah
  • 43
  • 8
SeanNerd
  • 116
  • 2
1

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

leeroya
  • 485
  • 7
  • 12
1

Update value through this code it's simply run console application that reads application settings, adds a new setting, and updates an existing setting. and after update refresh the application on server without closed application.

For more information: See Microsoft .Net Docs, ConfigurationManager.AppSettings Property

static void AddUpdateAppSettings(string key, string value)
    {
        try
        {
            var configFile = System.Web.Configuration.WebConfigurationManager.OpenWebConfiguration("~");
            var settings = configFile.AppSettings.Settings;
            if (settings[key] == null)
            {
                settings.Add(key, value);
            }
            else
            {
                settings[key].Value = value;
            }
            configFile.Save(ConfigurationSaveMode.Modified);
            ConfigurationManager.RefreshSection(configFile.AppSettings.SectionInformation.Name);
        }
        catch (ConfigurationErrorsException ex)
        {
            Console.WriteLine("Error writing app settings. Error: "+ ex.Message);
        }
    }
0

I solved similar problem - I needed override appSettings like this:

For 'IConfigurationBuilder':

configurationBuilder
            .AddJsonFile("appsettings.json", false, true)
            .AddJsonFile($"appsettings.{environmentName}.json", false, true)
            .AddConfigurationObject(TenantsTimeZoneConfigurationOverrides(configurationBuilder)); // Override Tenants TimeZone configuration due the OS platform (https://dejanstojanovic.net/aspnet/2018/july/differences-in-time-zones-in-net-core-on-windows-and-linux-host-os/)

 private static Dictionary<string, string> TenantsTimeZoneConfigurationOverrides(IConfigurationBuilder configurationBuilder)
    {
        var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 
        var overridesDictionary = new Dictionary<string, string>();
        var configuration = configurationBuilder.Build() as IConfiguration;
        var tenantsSection = configuration.GetSection(TenantsConfig.TenantsCollectionConfigSectionName).Get<Tenants>();
        foreach (var tenant in tenantsSection)
        {
            if (!string.IsNullOrEmpty(tenant.Value.TimeZone))
            {
                overridesDictionary.Add($"Tenants:{tenant.Key}:TimeZone", GetSpecificTimeZoneDueOsPlatform(isWindows, tenant.Value.TimeZone));
            }
        }
        return overridesDictionary;
    }

    private static string GetSpecificTimeZoneDueOsPlatform(bool isWindows, string timeZone)
    {
        return isWindows ? timeZone : TZConvert.WindowsToIana(timeZone);
    }
David
  • 81
  • 1