Search code examples
c#.net.net-coreconfigurationsystem.text.json

Using System.Text.Json to Serialize an IConfiguration back to Json


I'm storing some IConfiguration as json in my sqlserver db so I can then bind them to some already constructed classes in order to provide dynamic settings.

At some point I might change the binded properties new at runtime and then update the db. The thing is that when i need to, the class might have more properties that aren't supposed to be bound and shouln't be serialized. I am therefore keeping the IConfiguration as a property of my class. Another reason why I'm using this approach is that I need to istantiate other children classes from the class that has loaded the configs, and save them to db when i do.

The thing is that when I serialize an IConfiguration i only get an empty json string like "{}". I suppose i could do some shenanigans leveraging .AsEnumerable() but isn't there a better way?

My sample code would look somewhat like this

public class ConfigurableClass
{

    public int ChildrenCount { get; set; } = 1069;
    public bool IsFast { get; set; } = false;
    public bool HasChildren { get; set; } = false;

    public int Id { get; }

    public ConfigurableClass(int id) { Id = id; }
}

static void Main(string[] args)
{

    IEnumerable<string> configs = SqlConfigLoader.LoadConfig();

    foreach (var str in configs)
    {
        Console.WriteLine("Parsing new Config:");

        var builder = new ConfigurationBuilder();

        IConfiguration cfg = builder.AddJsonStream(new MemoryStream(Encoding.Default.GetBytes(str)))
                .Build();

        var stepExample = new ConfigurableClass(9);

        cfg.Bind(stepExample);

        //do work with the class that might change the value of binded properties                   

        var updatedCfg = cfg;

        Console.WriteLine(JsonSerializer.Serialize(updatedCfg));

        Console.WriteLine();
    }

    Console.ReadLine();
}

Edit

I Also tried a diffent approach, by converting the IConfiguration to a nested dictionary like this

ublic static class IConfigurationExtensions
{
   public static Dictionary<string,object> ToNestedDicionary(this IConfiguration configuration)
   {
       var result = new Dictionary<string, object>();
       var children = configuration.GetChildren();
       if (children.Any()) 
           foreach (var child in children)
               result.Add(child.Key, child.ToNestedDicionary());
       else 
           if(configuration is IConfigurationSection section)
               result.Add(section.Key, section.Get(typeof(object)));

       return result;
   }        
}

But I lose the implicit type behind a given JsonElement:

if i serialize the resulting dictionary i get thing like "Property": "True" instead of "Property" : true


Solution

  • First, the why

    Attempting to serialize the IConfiguration this way is not going to work how you want it to. Let's explore why.

    Serializing Interfaces

    Part of the reason you get no properties is because the generic type argument to Serialize is IConfiguration. In other words you are calling:

    JsonSerializer.Serialize<IConfiguration>(updatedCfg)
    

    When System.Text.Json serializes using a generic parameter it only (by default without any custom converters) serializes the public properties of that interface. In this case IConfiguration has no public properties (other than an indexer) so your output is empty json.

    Using runtime-type information

    Now, in general to get around this you would use the non-generic overload and pass the type. For example that would look like:

    JsonSerializer.Serialize(updatedCfg, updatedCfg.GetType());
    

    Or alternatively by using object as the type parameter:

    JsonSerializer.Serialize<object>(updatedCfg);
    

    System.Text.Json will then use the runtime type information in order to determine what properties to serialize.

    The ConfigurationRoot

    Now the second part of your problem is that this is unfortunately still not going to work due to how the configuration system is designed. The ConfigurationRoot class (the result of Build) can aggregate many configuration sources. The data is stored individually within (or even external to) each provider. When you request a value from the configuration it loops through each provider in order to locate a match.

    All of this to say that the concrete/runtime type of your IConfiguration object will still not have the public properties you desire to serialize. In fact, passing the runtime type in this case will do worse than mimic the behavior of the interface as it will attempt to serialize the only public property of that type (ConfigurationRoot.Providers). This will give you a list of serialized providers, each typed as IConfigurationProvider and having zero public properties.

    A potential solution

    Since you are attempt to serialize the configuration that you are ultimately binding to an object, a workaround would be to re-serialize that object instead:

    var stepExample = new ConfigurableClass(9);
    cfg.Bind(stepExample);
    var json1 = JsonSerializer.Serialize(stepExample, stepExample.GetType());
    // or with the generic version which will work here
    var json2 = JsonSerializer.Serialize(stepExample);
    

    An alternative solution - AsEnumerable

    IConfiguration is ultimately a collection of key value pairs. We can make use of the AsEnumerable extension method to create a List<KeyValuePair<string, string>> out of the entire configuration. This can later be deserialized and passed to something like AddInMemoryCollection

    You'll need the Microsoft.Extensions.Configuration.Abstractions package (which is likely already transitively referenced) and the following using directive:

    using Microsoft.Extensions.Configuration;
    

    And then you can create a list of all the values (with keys in Section:Key format)

    var configAsList = cfg.AsEnumerable().ToList();
    var json = JsonSerializer.Serialize(configAsList);
    

    Or you can use ToDictionary and serialize that instead.

    var configAsDict = cfg.AsEnumerable().ToDictionary(c => c.Key, c => c.Value);
    var json = JsonSerializer.Serialize(configAsDict);
    

    Both formats will work with AddInMemoryCollection as that only requires an IEnumerable<KeyValuePair<string, string>> (which both types are). However, you will likely need the Dictionary format if you wish to use AddJsonFile/Stream as I don't think those support an array of key/value pairs.

    Strings, strings and nothing but strings

    You seem to be under the impression that IConfiguration objects are storing ints, bools, etc. (for example) corresponding to the JSON Element type. This is incorrect. All data within an IConfiguration is stored in stringified form. The base Configuration Provider classes all expect an IDictionary<string, string> filled with data. Even the JSON Configuration Providers perform an explicit ToString on the values.

    The stringyly-typed values are turned into strongly-typed ones when you call Bind, Get<> or GetValue<>. These make use of the configuration binder which in turn uses registered TypeConverters and well know string parsing methods. But under the covers everything is still a string. This means it doesn't matter if your json file has a string property with value "True" or a boolean property with value true. The binder will appropriately convert the value when mapping to a boolean property.

    Using the above dictionary serializing method will work as intended.