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.