This is the first post in a series about things that you can do in JavaScript that are not so easy in C#… but just because they’re not easy, doesn’t mean they’re impossible!
Table of contents
Open Table of contents
Dynamic vs Static Types
In JavaScript, objects are flexible. They don’t have any set type, so they can contain whatever properties and methods you want and be molded, shaped, and extended with new properties at runtime, without any ceremony. This dynamism is something that C# developers might look at with a hint of envy.
The JavaScript Way
Imagine you have an object, and you want to add a new property to it. In JavaScript, it’s as simple as:
const obj = {};
obj["foo"] = "bar";
Or you can do the same using dot notation:
const obj = {};
obj.foo = "bar";
Either way, obj
ends up with a property named foo
containing the value bar
. No class definitions, no hassle.
The C# Approach
C# is a statically typed language. Properties are predefined, and objects are instances of classes.
So how do we achieve the above in C#?
Dictionaries
One way is to use a specific class that let’s you do this, like Dictionary<TKey, TValue>
:
var obj = new Dictionary<string, object?>();
obj["foo"] = "bar";
That works… and it probably mimics what’s going on in JavaScript most closely. Objects in JavaScript are essentially just dictionaries, with some dot notation added as syntactic sugar. So we could use dictionaries everywhere in C# and hack away just like front end devs. The only thing we’re really missing here is the nice dot notation.
And I guess that’s fine if you want to throw static typing out the window entirely and if you control the whole codebase. Sometimes we want to make use of classes that other developers have written though… possibly sealed classes in third party libraries - and not all of these will inherit from Dictionary<TKey, TValue>
😟
External Dictionary
To augment an instance of a class that we didn’t author and we can’t modify, one naive approach is to use Dictionary to map from that object to somewhere else where we hold the properties. So something like this (not thread safe and I wouldn’t recommend using it… but for demonstration purposes):
static class ObjectExtensions
{
private static readonly DynamicProperties Map = new DynamicProperties();
public static T? GetProperty<T>(this object source, string key) => (T?)Map[source][key];
public static void SetProperty(this object source, string key, object? value) => Map[source][key] = value;
class DynamicProperties
{
private readonly Dictionary<object, Dictionary<string, object?>> _properties = new();
public Dictionary<string, object?> this[object source]
{
get
{
if (_properties.TryGetValue(source, out var dict)) return _properties[source];
dict = new Dictionary<string, object?>();
_properties.Add(source, dict);
return _properties[source];
}
}
}
}
Having defined that somewhere in our application, we could now do this:
var myClass = new object();
myClass.SetProperty("foo", "bar");
There is a fundamental problem with this code however. ObjectExtensions.Map
will store a reference to any object that we add dynamic properties to and will prevent it from being garbage collected! 🙀
ConditionalWeakTable to the Rescue
Found in the System.Runtime.CompilerServices
namespace, the ConditionalWeakTable
:
Enables compilers to dynamically attach object fields to managed objects.
ConditionalWeakTable<TKey,TValue>
is a bit like Dictionary<TKey, TValue>
however it only holds weak references to all it’s keys… which means it doesn’t prevent any objects that are used as keys from being garbage collected (the references it holds aren’t considered by the garbage collector)… and when any objects that serve as key values pass out of scope, ConditionalWeakTable
auto-magically removes the associated values from it’s table! This is very cool and what makes ConditionalWeakTable
so powerful. You can see why it’s useful for compilers… but we can use it too 😎
Armed with the ConditionalWeakTable
then, we can modify our ObjectExtensions
to look like this instead:
static class ObjectExtensions
{
private static ConditionalWeakTable<object, Dictionary<string, object?>> Map { get; } = new();
private static Dictionary<string, object?> AssociatedProperties(this object source) =>
Map.GetValue(source, _ => new Dictionary<string, object?>());
public static T? GetProperty<T>(this object source, string key) => (T?)source.AssociatedProperties()[key];
public static void SetProperty(this object source, string key, object? value) => source.AssociatedProperties()[key] = value;
}
And now we can safely set and get arbitrary properties on any object, with no concern for static typing, so that our C# applications can crash at runtime just as often as their JavaScript cousins!
var myClass = new object();
myClass.SetProperty("foo", "bar");
Console.WriteLine($"Foo = {myClass.GetProperty<string>("foo")}");
We don’t get the fancy dot notation that you get with JavaScript but it’s still pretty cool and gives us equivalent functionality in terms of dynamically adding class members to arbitrary objects.
Source code and NuGet package
The above is a fairly minimal solution to the problem with no null
checks or anything… You can get the source code for a more polished solution at
https://github.com/mentaldesk/fuse or just add the MentalDesk.Fuse NuGet package to your solution so that you can start sprinkling dynamic properties all over the objects you use!
Words of caution
This post really should come with a warning label at the top (sorry about that - I put it at the bottom). Although you can add properties to objects in C# dynamically, you should probably do so with extreme caution and only when you can’t really find a better solution. Under the hood, we’re just using a fancy dictionary and there’s nothing to stop us doing this:
var myClass = new object();
myClass.SetProperty("foo", "bar");
… and then somewhere else in our code doing this:
myClass.SetProperty("foo", 42);
Which makes it problematic when we want to do this:
var wtf = myClass.GetProperty("foo");
Wrapping Up
ConditionalWeakTable
allows C# developers to do all those dangerous things that JavaScript programmers have been doing for years.
However with great power comes great potential confusion… Use these sparingly (ideally not at all - but I have bumped into two real world use cases for this myself in the past year). Keep your code clean and your developers sane.
Stay tuned for more adventures in doing dangerous stuff with C#.
Happy coding and may your objects always be just dynamic enough!