Paweł Górszczak
Paweł Górszczak

Reputation: 574

Save Changes of IConfigurationRoot sections to its *.json file in .net Core 2.2

i was digging to find out the solution but didn't manage to find it, i bet that someone has encountered this problem, so what is the problem?.

For test i have created simple console application (solution will be used in asp.net core web api).

I have TestSetting.json configuration file with 'Copy Always' setuped.

{
  "setting1" : "value1" 
}

And Simple code

IConfigurationBuilder configurationBuilder = new ConfigurationBuilder();

IConfigurationRoot configuration = configurationBuilder.AddJsonFile("TestSettings.json",false, reloadOnChange: true).Build();

Console.WriteLine(configuration.GetSection("setting1").Value); // Output: value1
//Change configuration manually in file while console is waiting
Console.ReadKey();

//Changed manually in file Value appears
Console.WriteLine(configuration.GetSection("setting1").Value); // Output: Whatever you have setuped
Console.ReadKey();

configuration.GetSection("setting1").Value = "changed from code";
//Changed in code value appear
Console.WriteLine(configuration.GetSection("setting1").Value); // Output: changed from code

I have 2 requirements, i want to make it possible to change value in json configuration file manually while application is running and application will see updated value during next get of setting Section and it is working.

Second requirement is that, i want to preserve some information, to be accurate a last execution time of task which should be executed once per setuped period ex. once a day, so some loop check last execution time value and determine if operation has to be executed. Someone would ask that what i have will work, but i need also to cover scenario when operation has been executed and application has been restarted (server error, user restart etc), and i need to save this information in a way which will allow me to read it after app startup.

Reading code sample we can see that after changing setting1 in code we see that this section has been changed while trying to output it to console.

configuration.GetSection("setting1").Value = "changed from code";
//Changed in code value appear
Console.WriteLine(configuration.GetSection("setting1").Value); // Output: changed from code

Here comes the question :). Is it possible that this settings section change will also affect actual value in json file? I don't want to manually change this file by some stream writers or whatever.

Actual result is that: after changing value in code, the new value is getable in runtime but when you will go to debug binaries you will se that value1 in TestSettings.json file hasnt been changed.

Upvotes: 10

Views: 14783

Answers (5)

BerndK
BerndK

Reputation: 1090

I think it is a bit oversized to write a new provider.

I just wrote a small helper that looks for the last registered JsonConfiguration provider and stores the current data to the related file. The file is created if not existing. I tried to be minimal invasive - this should work by just using the standard provider (.AddJsonFile), should also work with AOT.

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

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.Json;
using Microsoft.Extensions.FileProviders;

namespace Microsoft.Extensions.Configuration.Json
{
  public static class JsonConfigurationHelper
  {
    public static bool SaveJsonProvider(this IConfigurationRoot configurationRoot)
    {
      //try to get the last registered json provider
      var provider = configurationRoot.Providers.LastOrDefault(p => p is JsonConfigurationProvider) as JsonConfigurationProvider;
      if (provider == null)
        return false;

      var filepath = Filepath(provider);
      if (filepath == null)
        return false;

      string? sJson = File.Exists(filepath)
        ? File.ReadAllText(filepath)
        : null;

      var rootNode = sJson != null
        ? JsonNode.Parse(sJson, documentOptions: new JsonDocumentOptions() {CommentHandling = JsonCommentHandling.Skip})
        : new JsonObject();

      var rootObj = rootNode?.AsObject();
      if (rootObj == null)
        return false;

      var earlierKeys = Enumerable.Empty<string>();

      void HandleKeys(JsonObject parentNode, string? parentPath)
      {
        foreach (var key in provider.GetChildKeys(earlierKeys, parentPath).Distinct())
        {
          var fullKey = parentPath != null
            ? ConfigurationPath.Combine(parentPath, key)
            : key;
          if (provider.TryGet(fullKey, out var sValue))
          {
            var jsonValue = JsonValue.Create(sValue);
            parentNode[key] = jsonValue;
          }
          else
          {
            //probably a section
            var node = parentNode[key];
            var sectionObj = node == null
              ? (parentNode[key] = new JsonObject()).AsObject()
              : node.AsObject(); //this will throw if the node is not an object (e.g. there is a value with that key)
            HandleKeys(sectionObj, fullKey);
          }
        }
      }
      
      HandleKeys(rootObj,null);

      //save the json to file
      var options = new JsonSerializerOptions() { WriteIndented = true, TypeInfoResolver = JsonConfigurationHelperSourceGenerationContext.Default };
      sJson = rootNode?.ToJsonString(options);
      File.WriteAllText(filepath, sJson);
      return true;
    }

    internal static string? Filepath(FileConfigurationProvider fileConfigurationProvider)
    {
      IFileInfo? file = fileConfigurationProvider.Source.FileProvider?.GetFileInfo(fileConfigurationProvider.Source.Path ?? string.Empty);
      if (file == null)
        return null;
      return file.PhysicalPath;
    }
  }

  [JsonSourceGenerationOptions(WriteIndented = true)]
  [JsonSerializable(typeof(string))]
  internal partial class JsonConfigurationHelperSourceGenerationContext : JsonSerializerContext
  {
  }
}

see this full featured sample on how to use it in a DI context:

[Test]
public void JsonConfigUpdateTest()
{
  //please make sure that your app has the right to write in this folder (typically not the case as it is in Program Files for normal user)
  //consider to use e.g. ProgramData or AppData folder
  var startupPath = System.AppContext.BaseDirectory?.TrimEnd(Path.DirectorySeparatorChar)
                    ?? throw new InvalidOperationException();

  var config = new ConfigurationManager();
  config
    .SetBasePath(startupPath)
    .AddJsonFile("settings.json", optional:true);

  ServiceCollection services = new ServiceCollection();
  services.AddSingleton<IConfigurationRoot>(config);
  services.AddSingleton<IConfiguration>(config);
  //... add other services
  var serviceProvider = services.BuildServiceProvider();

  //...

  var configRoot = serviceProvider.GetRequiredService<IConfigurationRoot>();
  var section = configRoot.GetSection("UserSettings");
  section["LastFolderUsed"] = "C:\\Temp";
  configRoot.SaveJsonProvider(); //this will save the changes to the json file, file is created if not exists
}

Upvotes: 0

Troy
Troy

Reputation: 567

I needed to be able to set a value on an object multiple levels down the configuration structure, so I wrote a little extension method to do it.

It uses JsonObject and JsonNode from System.Text.Json.Nodes but should be pretty easy to switch to JObject and JValue from the Newtonsoft Package.

I used the extension method to replace the line jsonObj[key] = value; from the answer by @Pawel

internal static bool SetValue(this JsonObject obj, string key, JsonNode? value, string[]? delimiters = null)
{
    var retVal = false;

    if (obj is not null)
    {
        if (delimiters is null)
        {
            // These are the default delimiters supported by the .Net Configuration
            delimiters = new string[] { ":", "__" };
        }

        var keyParts = key.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);

        JsonObject contextObj = obj;

        for (var i = 0; i < keyParts.Length - 1; i++)
        {
            JsonNode? nextContextObj = contextObj[keyParts[i]];
            if (nextContextObj is null)
            {
                nextContextObj = contextObj[keyParts[i]] = new JsonObject();
            }
            contextObj = nextContextObj.AsObject();
        }

        if (contextObj is not null)
        {
            contextObj[keyParts[keyParts.Length - 1]] = value;
            retVal = true;
        }
    }

    return retVal;
}

Upvotes: 0

Philip T.
Philip T.

Reputation: 29

First of all thanks for the answer on this Pawel, your override method helped me a lot.

This is related to the answer from Pawel and the comment from Kirk Hawley

@Kirk Hawley

It works for sections you just need to adjust the override to match with your json pattern.

You can get the path from a section with ConfigurationSection.Path.

public static string KeyPairPath { get; set; }
MyPublicClass.KeyPairPath = mysection.Path;

As the key parameter of the override function only contains the last key and not the full key (with the parent section) if you use child sections, you can hand over the full path from the section as a static member of a class. For example:

public override void Set(string key, string value)
{
            string currentPath = MyPublicClass.KeyPairPath;

            string[] getParent = currentPath.Split(':');
            string parent = getParent[0];

            key = parent + ":" + key;

            string[] substantialKey = key.Split(":");
            
            base.Set(key, value);

            var fileFullPath = base.Source.FileProvider.GetFileInfo(base.Source.Path).PhysicalPath;
            string json = File.ReadAllText(fileFullPath);
            dynamic jsonObj = JsonConvert.DeserializeObject(json);
            jsonObj[parent][substantialKey[1]] = value;

            string output = JsonConvert.SerializeObject(jsonObj, Formatting.Indented);
            File.WriteAllText(fileFullPath, output);
}

This worked for me in order to update my appsettings.json.

Upvotes: 2

Paweł G&#243;rszczak
Paweł G&#243;rszczak

Reputation: 574

Thank you "Matt Luccas Phaure Jensen" For this, information, i have't found any solution for this there. If any one wants to use IOptions here is an answer how to do it How to update values into appsetting.json?

I want to do it in the way i started, so i looked to the implementation of Microsoft.Extensions.Configuration.Json and i have prepared simple solution to allow writing and use base implementation. It probably has many limitations but it will work in simple scenarios.

Two implementations from above dll have to be extended. So lets do it.

Files to create

Implementation of WritableJsonConfigurationProvider with example save code of desired section.

Change values in JSON file (writing files)

public class WritableJsonConfigurationProvider : JsonConfigurationProvider
{
    public WritableJsonConfigurationProvider(JsonConfigurationSource source) : base(source)
    {
    }

    public override void Set(string key, string value)
    {
        base.Set(key,value);

        //Get Whole json file and change only passed key with passed value. It requires modification if you need to support change multi level json structure
        var fileFullPath = base.Source.FileProvider.GetFileInfo(base.Source.Path).PhysicalPath;
        string json = File.ReadAllText(fileFullPath);
        dynamic jsonObj = JsonConvert.DeserializeObject(json);
        jsonObj[key] = value;
        string output = JsonConvert.SerializeObject(jsonObj, Formatting.Indented);
        File.WriteAllText(fileFullPath, output);
    }
}

And implementation of WritableJsonConfigurationSource which is a extention of JsonConfigurationSource

public class WritableJsonConfigurationSource : JsonConfigurationSource
{
    public override IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        this.EnsureDefaults(builder);
        return (IConfigurationProvider)new WritableJsonConfigurationProvider(this);
    }
}

and that's it, lets use it

IConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
IConfigurationRoot configuration = configurationBuilder.Add<WritableJsonConfigurationSource>(
    (Action<WritableJsonConfigurationSource>)(s =>
                                                     {
                                                         s.FileProvider = null;
                                                         s.Path = "TestSettings.json";
                                                         s.Optional = false;
                                                         s.ReloadOnChange = true;
                                                         s.ResolveFileProvider();
                                                     })).Build();

Console.WriteLine(configuration.GetSection("setting1").Value); // Output: value1
Console.ReadKey();

configuration.GetSection("setting1").Value = "changed from codeeee";
Console.WriteLine(configuration.GetSection("setting1").Value); // Output: changed from codeeee

Console.ReadKey();

Value is being changed in memory as well in file. Bingo :).

The code could have problems, can be refactored etc, this is only sample quick solution.

Upvotes: 15

It is not possible via the Microsoft.Extensions.Configuration package to save changes made to the configuration to disk. There is an issue about it on github here where they chose not to do it. It is possible to do, just not via the IConfiguration interface. https://github.com/aspnet/Configuration/issues/385

Upvotes: 5

Related Questions