5

In my appSettings.json, I have a configuration section that can contain anything as long as it's json valid. It is usally a set of key/value (string/string)

I would like to get it in my code and return it in a controller call.

I took a look at the source code (https://github.com/aspnet/Configuration/blob/6d9519622b5db2c5ac6bafa8bcdb25fe27914de3/src/Config.Binder/ConfigurationBinder.cs ) and it seems I am doomed with off-the-shelves solutions.

If I limit the use case to key value pairs, I can use the AsEnumerable() in the IConfigSection and that's fine. If I want to allow lists, then I may still be ok parsing the keys to look for :Number but does someone has a way to easily deserialize a random object ? Or even better get the configuration section as is without deserializing it.

For example

{
 "mySettings": 
 {
   "key1": "value1",
   "key2": "value2",
   "list": [ "item1", "item2", "item3" ],
   "complexObject": {
     "key": "value",
     "anything" :  [{"id": "3", "name": "John"}]
   }
 }
}
Benjamin Baumann
  • 4,035
  • 2
  • 25
  • 35

1 Answers1

5

It's possible if you abuse .NET 4 dynamic objects. As you said, you can enumerate over all keys in the config, and they all follow the same pattern. With your example, all the keys of interest are:

mySettings null 
mySettings:list null 
mySettings:list:2 item3 
mySettings:list:1 item2 
mySettings:list:0 item1 
mySettings:key3 value3 
mySettings:key2 value2 
mySettings:key1 value1 
mySettings:complexObject null 
mySettings:complexObject:key value 
mySettings:complexObject:anything null 
mySettings:complexObject:anything:0 null 
mySettings:complexObject:anything:0:name John 
mySettings:complexObject:anything:0:id 3 

From this, we can build an ExpandoObject, like so:

[HttpGet]
public IActionResult Get([FromServices] IConfiguration config)
{
    var result = new ExpandoObject();

    // retrieve all keys from your settings
    var configs = config.AsEnumerable().Where(_ => _.Key.StartsWith("mySettings"));
    foreach(var kvp in configs) 
    {
        var parent = result as IDictionary<string, object>;
        var path = kvp.Key.Split(':');

        // create or retrieve the hierarchy (keep last path item for later)
        var i = 0;
        for (i = 0; i < path.Length - 1; i++)
        {
            if (!parent.ContainsKey(path[i]))
            {
                parent.Add(path[i], new ExpandoObject());
            }

            parent = parent[path[i]] as IDictionary<string, object>;
        }

        if (kvp.Value == null)
            continue;

        // add the value to the parent
        // note: in case of an array, key will be an integer and will be dealt with later
        var key = path[i];
        parent.Add(key, kvp.Value);
    }

    // at this stage, all arrays are seen as dictionaries with integer keys
    ReplaceWithArray(null, null, result);

    return Ok(result);
}

private void ReplaceWithArray(ExpandoObject parent, string key, ExpandoObject input) 
{
    if (input == null)
        return;

    var dict = input as IDictionary<string, object>;
    var keys = dict.Keys.ToArray();

    // it's an array if all keys are integers
    if (keys.All(k => int.TryParse(k, out var dummy))) {
        var array = new object[keys.Length];
        foreach(var kvp in dict) {
            array[int.Parse(kvp.Key)] = kvp.Value;
            // Edit: If structure is nested deeper we need this next line 
            ReplaceWithArray(input, kvp.Key, kvp.Value as ExpandoObject);
        }

        var parentDict = parent as IDictionary<string, object>;
        parentDict.Remove(key);
        parentDict.Add(key, array);
    }
    else
    {
        foreach (var childKey in dict.Keys.ToList())
        {
            ReplaceWithArray(input, childKey, dict[childKey] as ExpandoObject);
        }
    }
}

Note: since the colon : is used as separator, you can't have a key that contains a colon.

Finally, because you now have a dynamic object, you can directly get its properties:

dynamic asDym = result;
string name = asDym.mySettings.complexObject.anything[0].name;
Eddy
  • 5,320
  • 24
  • 40
Métoule
  • 13,062
  • 2
  • 56
  • 84
  • Works like a charm! One question though, what's the benefit of using ExpandoObject instead of Dictionary here? I ran tests and both seems to work. – Benjamin Baumann Apr 25 '18 at 09:02
  • 2
    Using `ExpandoObject` allows you to use `result` as a true dynamic object, so that you can access any member with the dot syntax (see the example at the end of my answer). If you don't need to access individual properties in your controller, and you simply want the entire object, you can use dictionaries. – Métoule Apr 25 '18 at 09:08