10

I have the following code in my Program.cs:

var configuration = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("clientsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"clientsettings.{host.GetSetting("environment")}.json", optional: true, reloadOnChange: true)
                .AddEnvironmentVariables()
                .Build();

I want to convert the result of building my configuration to JObject\Json for sending to the client. How can I do it? and I don't want to create my custom class for my settings.

My answer: merge

public static JObject GetSettingsObject(string environmentName)
    {
        object[] fileNames = { "settings.json", $"settings.{environmentName}.json" };


        var jObjects = new List<object>();

        foreach (var fileName in fileNames)
        {
            var fPath = Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + fileName;
            if (!File.Exists(fPath))
                continue;

            using (var file = new StreamReader(fPath, Encoding.UTF8))
                jObjects.Add(JsonConvert.DeserializeObject(file.ReadToEnd()));
        }


        if (jObjects.Count == 0)
            throw new InvalidOperationException();


        var result = (JObject)jObjects[0];
        for (var i = 1; i < jObjects.Count; i++)
            result.Merge(jObjects[i], new JsonMergeSettings
            {
                MergeArrayHandling = MergeArrayHandling.Merge
            });

        return result;
    }
heoLixfy
  • 121
  • 1
  • 2
  • 11
  • 1
    `sending to the client`? What does client mean here? – TanvirArjel Jan 10 '19 at 13:38
  • Browser/js code – heoLixfy Jan 10 '19 at 13:41
  • why not send just the apsettings.json file? – federico scamuzzi Jan 10 '19 at 13:42
  • `Browser/js code` means you want to access the configuration from view? – TanvirArjel Jan 10 '19 at 13:42
  • @federicoscamuzzi i want to merge appsettings.json and appsettings.{host.GetSetting("environment")}.json to one object. – heoLixfy Jan 10 '19 at 13:44
  • I solved similar problem by reading the configuration file as text and parsing as `JObject`. You can match particular properties by the path. You just need to convert `IConfiguration` path to `JObject` path by replacing ":\(d+)" to "{1}" then ':' to '.'. I use this approach to extract configuration sections as JSON strings. Obviously - this approach works only when your original configuration is a JSON file. Otherwise you will have to build JObject yourself from the graph. – Harry Mar 12 '21 at 13:46

6 Answers6

6

Since configuration is actually just a key value store where the keys have a certain format to represent a path, serializing it back into a JSON is not that simple.

What you could do is recursively traverse through the configuration children and write its values to a JObject. This would look like this:

public JToken Serialize(IConfiguration config)
{
    JObject obj = new JObject();
    foreach (var child in config.GetChildren())
    {
        obj.Add(child.Key, Serialize(child));
    }

    if (!obj.HasValues && config is IConfigurationSection section)
        return new JValue(section.Value);

    return obj;
}

Note that this is extremely limited in how the output looks. For example, numbers or booleans, which are valid types in JSON, will be represented as strings. And since arrays are represented through numerical key paths (e.g. key:0 and key:1), you will get property names that are strings of indexes.

Let’s take for example the following JSON:

{
  "foo": "bar",
  "bar": {
    "a": "string",
    "b": 123,
    "c": true
  },
  "baz": [
    { "x": 1, "y": 2 },
    { "x": 3, "y": 4 }
  ]
}

This will be represented in configuration through the following key paths:

"foo"      -> "bar"
"bar:a"    -> "string"
"bar:b"    -> "123"
"bar:c"    -> "true"
"baz:0:x"  -> "1"
"baz:0:y"  -> "2"
"baz:1:x"  -> "3"
"baz:1:y"  -> "4"

As such, the resulting JSON for the above Serialize method would look like this:

{
  "foo": "bar",
  "bar": {
    "a": "string",
    "b": "123",
    "c": "true"
  },
  "baz": {
    "0": { "x": "1", "y": "2" },
    "1": { "x": "3", "y": "4" }
  }
}

So this will not allow you to get back the original representation. That being said, when reading the resulting JSON again with Microsoft.Extensions.Configuration.Json, then it will result in the same configuration object. So you can use this to store the configuration as JSON.

If you want anything prettier than that, you will have to add logic to detect array and non-string types, since both of these are not concepts of the configuration framework.


I want to merge appsettings.json and appsettings.{host.GetSetting("environment")}.json to one object [and send that to the client]

Keep in mind that environment-specific configuration files often contain secrets that shouldn’t leave the machine. This is also especially true for environment variables. If you want to transmit the configuration values, then make sure not to include the environment variables when building the configuration.

poke
  • 369,085
  • 72
  • 557
  • 602
  • If i am getting. a configuration update at runtime via eventbus, and i want to use this new configuration, can I use ICOnfigurationRoot instead of IConfiguration ? I am parsing the incoming data and building via COnfgiurationBuilder, but i am not sure if ICOnfigurationRoot can be used in place of IConfiguration, which is the default injected in services !! – kuldeep Jun 23 '20 at 15:39
  • @kuldeep If you want to consume configuration from a custom source, you should look into writing your own configuration source/provider. That way, you can utilize the existing default Infrastructure for configurations and just build on top of that. You can also support updating the config that way. – poke Jun 23 '20 at 22:56
  • Thank you, So far the custom configuration I have seen in samples can be only loaded at hostbuilder in program.cs via configureAppConfiguration. How can I load my custom configuration in middle of application based on some event published on bus? – kuldeep Jun 24 '20 at 03:51
  • 1
    Yeah, the configuration is most commonly configured in a central location and you would set up your configuration providers there. This is primarily because the configuration applies to many parts of the application; so you will need to read all configuration sources at the beginning. – If you don’t want that, then you will probably have to load the configuration separately. If you want to reuse the configuration library, you could always create a separate configuration root by creating a new configuration builder with only your source. You will have to manage that yourself then though. – poke Jun 25 '20 at 10:17
  • Thanks, this is what I end up doing. I build a configuration root and sets up my configuration manually via some methods i have defined, I was just not sure if i am doing it right – kuldeep Jun 25 '20 at 10:55
4

Here is Tom's solution converted to use System.Text.Json.

static internal JsonNode? Serialize(IConfiguration config)
{
    JsonObject obj = new();

    foreach (var child in config.GetChildren())
    {
        if (child.Path.EndsWith(":0"))
        {
            var arr = new JsonArray();

            foreach (var arrayChild in config.GetChildren())
            {
                arr.Add(Serialize(arrayChild));
            }

            return arr;
        }
        else
        {
            obj.Add(child.Key, Serialize(child));
        }
    }

    if (obj.Count() == 0 && config is IConfigurationSection section)
    {
        if (bool.TryParse(section.Value, out bool boolean))
        {
            return JsonValue.Create(boolean);
        }
        else if (decimal.TryParse(section.Value, out decimal real))
        {
            return JsonValue.Create(real);
        }
        else if (long.TryParse(section.Value, out long integer))
        {
            return JsonValue.Create(integer);
        }

        return JsonValue.Create(section.Value);
    }

    return obj;
}


// Use like this...
var json = Serialize(Config);
File.WriteAllText("out.json",
    json.ToJsonString(new JsonSerializerOptions() { WriteIndented = true}));
DrBB
  • 133
  • 6
3

The configuration data is represented by a flattened collection of KeyValuePair<string, string>. You could create a dictionary from it and serialize that to JSON. However, that will probably not give you the desired result:

Configuration.AsEnumerable().ToDictionary(k => k.Key, v => v.Value);

Also, please take in mind that this configuration object will contain environment variables, you definitely don't want to send these to the client.

A better option might be to first bind the configuration to your POCO's and serialize those to JSON:

var appConfig = new AppConfig();
Configuration.Bind(appConfig);
var json = JsonConvert.SerializeObject(appConfig);

public class AppConfig
{
    // Your settings here

    public string Foo { get; set; }

    public int Bar { get; set; }
}
Henk Mollema
  • 44,194
  • 12
  • 93
  • 104
3

The resultant IConfiguration object from the Build() method will encompass all of your configuration sources, and will merge based on the priority order defined by the order in which you added your config sources.

In your case this would be:

  • clientsettings.json
  • clientsettings.env.json
  • Environment Variables

You wont need to worry about merging sources manually or loading the files, as its already done for you.

To improve on poke's answer, I came up with this:

    private JToken Serialize(IConfiguration config)
    {
        JObject obj = new JObject();

        foreach (var child in config.GetChildren())
        {
            if (child.Path.EndsWith(":0"))
            {
                var arr = new JArray();

                foreach (var arrayChild in config.GetChildren())
                {
                    arr.Add(Serialize(arrayChild));
                }

                return arr;
            }
            else
            {
                obj.Add(child.Key, Serialize(child));
            }
        }

        if (!obj.HasValues && config is IConfigurationSection section)
        {
            if (bool.TryParse(section.Value, out bool boolean))
            {
                return new JValue(boolean);
            }
            else if (decimal.TryParse(section.Value, out decimal real))
            {
                return new JValue(real);
            }
            else if (long.TryParse(section.Value, out int integer))
            {
                return new JValue(integer);
            }

            return new JValue(section.Value);
        }

        return obj;
    }

The code above accounts for data types such as boolean, long & decimal.

long & decimal are the largest data types available for integers so will encompass any smaller values like short or float.

The code will also construct your arrays properly, so you end up with a like for like representation of all of your config in one json file.

Tom Day
  • 31
  • 2
  • This is perfect, and I think should be the accepted answer. I added another Answer which uses System.Text.Json.Nodes. – DrBB Aug 05 '22 at 16:34
1

Do you really want to sent to client all your environment variables (.AddEnvironmentVariables()), connections string and all other stuff in appsettings??? I recommend you do not do this.

Instead, make one class (say ClientConfigOptions), configure it binding using services.Configure<ClientConfigOptions>(configuration.GetSection("clientConfig")) and send it to client.

With this approach, you may also tune your ClientConfigOptions with Actions, copy some values from different appsetting paths, etc.

Dmitry
  • 16,110
  • 4
  • 61
  • 73
0

Just adding my approach to the list in case people find it easier to understand the flow:

public static class ConfigurationExtensions
{
    public static JObject ToJson(this IConfigurationRoot root)
    {
        var jObject = new JObject();
        foreach (var child in root.GetChildren())
        {
            jObject.Add(new JProperty(child.Key, child.ToJson()));
        }

        return jObject;
    }

    public static JToken ToJson(this IConfigurationSection section)
    {
        // if value
        if (section.Value != null)
        {
            if (bool.TryParse(section.Value, out var boolValue))
            {
                return new JValue(boolValue);
            }

            if (float.TryParse(section.Value, out var floatValue))
            {
                return new JValue(floatValue);
            }

            return new JValue(section.Value);
        }

        // if array
        if (section.GetChildren().FirstOrDefault()?.Key == "0")
        {
            return new JArray(section.GetChildren().Select(c => c.ToJson()));
        }

        // if object
        var jObject = new JObject();
        foreach (var child in section.GetChildren())
        {
            jObject.Add(new JProperty(child.Key, child.ToJson()));
        }

        return jObject;
    }
}
Michael
  • 11,571
  • 4
  • 63
  • 61