0

I apologize for the probably stupid question. Is there an easy way to define a key => value array with multiple levels and value types in C#?

I have an example here in javascript:

let keyValueArray = {
    key1: {
        key11: {
            key111: "String1",
            key112: "String2",
            key113: "String3"
        },
        key12: {
            key121: "String4"
        }
    },
    key2: {
        key21: {
            key211: 1234 //int
        }
    },
    key3: "String5"
}

I want to be able to get the values like this: var value = keyValueArray.key1.key11.key111;

I've tried quite a bit, but to no avail.

Thank you all

sepp89117
  • 11
  • 2
  • Your "key-value" collection is missing actual keys, you know. There's this group of string values under `key11`, but there's no actual key associated with them. This looks more like some kind of tree nodes. I mean, `key1` isn't an actual key; it's a collection of everything under `key11` and `key12`. – Nyerguds Aug 28 '21 at 10:02

1 Answers1

0

This question goes to the heart of a fundamental difference between JavaScript and many other object oriented languages including C#.

C# is statically typed, this means that (nearly - I'll come back to this) every object has a type which is fixed at compile-time, that type has a fixed set of methods and properties, and any attempt to reference a method or property which isn't part of the type's definition results in a compiler error. You can do things like declaring a variable as an object and then storing an instance of a derived type such as DateTime in it. If you do this, then calling the object's GetType() method (which is inherited from object) will tell you that the object's runtime type is System.DateTime. If you want to use the object's DateTime methods and properties then you first need to cast the object to a DateTime.

var myDate = (DateTime)myObject;
Console.WriteLine myDate.Year;

But C# (usually, I will get to this) won't let you change the type of an object after it's been declared, and it won't let you add properties at runtime.

JavaScript, by contrast, treats objects completely differently. Anything which isn't one of the primitive types (string, number, etc) is just an object and you can add and remove properties and methods at runtime. Attempting to get the value of a property which doesn't exist doesn't throw a runtime error, instead it just returns the value undefined. This makes JavaScript very powerful, but also prone to typos in property names resulting in runtime errors to the effect that undefined doesn't have a doSomething method.

So, how can create and use this type of tree data structure in C#? I'll give you 4 possible approaches.

System.Collections.Generic.Dictionary<TKey, TValue>

My first thought was to use a dictionary with string as the key and object as the value - this allows you to set the value to a string, integer or just about anything you want, including another dictionary, which allows you to build a tree structure.

public static void UseDictionary()
{
    Console.WriteLine("Using Dictionary<string, object>");
    var key11 = new Dictionary<string, object>
    {
        { "key111", "String1" },
        { "key112", "String2" },
        { "key113", "String3" }
    };

    var key12 = new Dictionary<string, object>
    {
        { "key121", "String4" }
    };

    var key1 = new Dictionary<string, object>
    {
        { "key11", key11 },
        { "key12", key12 }
    };

    var key21 = new Dictionary<string, object>
    {
        { "key211", 1234 }
    };

    var key2 = new Dictionary<string, object>
    {
        { "key21", key21 }
    };

    var keyValueArray = new Dictionary<string, object>
    {
        { "key1", key1 },
        { "key2", key2 },
        { "key3", "String5" }
    };

    var something = ((Dictionary<string, object>)((Dictionary<string, object>)keyValueArray["key1"])["key11"])["key111"];
    Console.WriteLine(something);

    var k1 = (Dictionary<string, object>)keyValueArray["key1"];
    var k11 = (Dictionary<string, object>)k1["key11"];
    var k111 = k11["key111"];
    Console.WriteLine(k111);
}

This works, but it's far from ideal. In particular, getting the value of something which is nested quite deep in the tree is just ugly and difficult to read.

var something = ((Dictionary<string, object>)((Dictionary<string, object>)keyValueArray["key1"])["key11"])["key111"];

And even breaking it down to several operations for readability is rather clunky

var k1 = (Dictionary<string, object>)keyValueArray["key1"];
var k11 = (Dictionary<string, object>)k1["key11"];
var k111 = k11["key111"];
Console.WriteLine(k111);

System.Dynamic.ExpandoObject

Here we meet the dynamic keyword. Initialising a variable with dynamic rather than var tells the compiler not to check at compile time whether or not the properties exist, thereby allowing you to add properties to an object at runtime. This is much closer to how JavaScript behaves. A consequence of this (in my Visual Studio installation anyway) is that it also turns off intellisense and auto-complete for that variable.

public static void UseExpandoObject()
{
    Console.WriteLine("Using ExpandoObject");
    dynamic key11 = new ExpandoObject();
    key11.key111 = "String1";
    key11.key112 = "String2";
    key11.key113 = "String3";

    dynamic key12 = new ExpandoObject();
    key12.key121 = "String4";

    dynamic key1 = new ExpandoObject();
    key1.key11 = key11;
    key1.key12 = key12;

    dynamic key21 = new ExpandoObject();
    key21.key211 = 1234;

    dynamic key2 = new ExpandoObject();
    key2.key21 = key21;

    dynamic keyValueArray = new ExpandoObject();
    keyValueArray.key1 = key1;
    keyValueArray.key2 = key2;
    keyValueArray.key3 = "String5";

    Console.WriteLine(keyValueArray.key1.key11.key111);
}

This is a big improvement, particularly the syntax for retrieving the value of a deeply nested key, although creating the tree is still fairly clunky.

System.Dynamic.ExpandoObject with JSON

Assuming that most of the time your data will be coming from a file, database, web service or some other external source, rather than being embedded in your code, you can now take advantage of JSON deserialisation to initialise your object. In this case I just copied your JSON into a file called data.json.

I've used the Newtonsoft.Json nuget package to do the deserialisation - this is just personal preference, there are other ways of deserialising JSON to objects.

public static void UseExpandoObjectWithJson()
{
    Console.WriteLine("Using ExpandoObject with JSON");
    var json = File.ReadAllText("data.json");
    dynamic keyValueArray = JsonConvert.DeserializeObject<ExpandoObject>(json);
    Console.WriteLine(keyValueArray.key1.key11.key111);
}

Nice, but it still doesn't behave exactly like JavaScript - if you try to reference a non-existent property of an ExpandoObject, it will throw a runtime error. To get non-existent properties to return null (probably the closest C# has to undefined) instead of throwing an error, you need to create your own implementation of the DynamicObject abstract class.

System.Dynamic.DynamicObject with JSON

public class Node : DynamicObject
{
    private Dictionary<string, object> properties = new Dictionary<string, object>();

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        if (properties.ContainsKey(binder.Name))
        {
            result = properties[binder.Name];
            return true;
        }
        else
        {
            result = null;
            return false;
        }
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        properties[binder.Name] = value;
        return true;
    }
}

Usage:

public static void UseDynamicObjectWithJson()
{
    Console.WriteLine("Using DynamicObject with JSON");
    var json = File.ReadAllText("data.json");
    dynamic keyValueArray = JsonConvert.DeserializeObject<Node>(json);
    Console.WriteLine(keyValueArray.key1.key11.key111);
}

Just be careful to test whether a property value is returning null before trying to access its properties, just as you'd need to in JavaScript if there was a chance that it might be undefined

Further reading

Very good article on the dynamic keyword, ExpandoObject and DynamicObject and kudos to this answer which directed me towards that article.

sbridewell
  • 777
  • 4
  • 16