Dynamic Data in the Statically Typed World of C#
Hi Friends.
I’ve been doing Web development for more than 10 years now. In that time, I’ve almost exclusively worked in C# when writing backend code. I’ve not had to work with dynamically typed data all that much – just twice in total at the time of writing this. Me and my team would typically create a statically typed C# object on the backend to represent data coming in from a Web client, we’d declare it as a controller method parameter type, and any incoming JSON sent to that endpoint would be deserialized into it.
I came across the need to use dynamically typed data for the second time while working in a recent project, and I thought it might be helpful to share the experiences.
The First Encounter
The first time that I encountered the need to use dynamically typed data was a few years ago. I was writing my first backend service that was built using a CQRS/Event Sourcing pattern.
In this pattern, changes to a data-model state are effectively written to the database as a series of simple and atomic commands. In other words, we give (and store) instructions detailing what to change in the data and how. Getting the most recent object state then becomes simple: we initialise a new data model (which would have an original and unmodified object state), and sequentially apply all available updates in the order that they were made.
Having the history of how a data model is modified also means that implementing undo/redo becomes relatively easy. We only need to choose how much of the history we replay onto the model.
There are the disadvantages of being more complex and using more processing power to rebuild models. However, a major advantage is that more robust concurrent systems can be built.
This approach contrasts with writing objects directly to the database and later loading them in their modified state. To see how this is not suitable for a system used by many users simultaneously, consider two people – Alice and Bob – wanting to work together on a shared spreadsheet. Suppose we have a system that loads data, mutates it, and then saves it back to the database. Alice updates a value in cell A1 at the same time as Bob updates cell B2. When saving the updates, there is a chance that both Alice and Bob will load the same original spreadsheet state, update it with their respective edits, and then save the updated (and complete) state back to the database. Let’s say Alice’s update gets saved first. Bob’s update arrives afterwards and gets saved to the database, overwriting Alice’s. As each update was based on the original spreadsheet state before any edits were made, Alice’s update would be lost.
The models in the system that I was building had properties of varying data type. There was one common command for updating the models, which would also specify the new value. As the update could target any of the data properties, using a static type for the incoming data was not practical.
The Second Encounter
My second encounter with dynamically typed data was much more recent. I was working on an API where the data it would receive didn’t conform to a strict data contract. Building the Typescript client was relatively easy: while Typescript makes use of data types, it’s possible to add additional properties to data. The difficulty came in trying to enumerate and use the data within the C# API.
Luckily, I remembered using the dynamic
keyword from before. By typing the data as dynamic
and casting it to a JObject
(it was being deserialized using Json.NET), I was able to safely enumerate the data’s keys and access their corresponding values. All was good. That is until I refactored it for reuse from elsewhere within the same API. To be clear, the refactor itself wasn’t the problem – so what was the issue?
The caller of the newly created service was passing in data that was dynamically built up with ExpandoObject
instances. This is a perfectly valid thing to do, and the project compiled quite happily. However, the API would throw a RuntimeBinderException
at runtime. The refactored service was originally built to work with JObject
instances (which was valid at the time of creation). However, it now had ExpandoObject
data being passed to it.
JObject
and ExpandoObject
are both examples of classes capable of storing dynamically typed data. They don’t share a common base type though, and this combined with inappropriate type casting was the cause of the RuntimeBinderException
errors. What’s worse was that the data was typed as dynamic
and so was being compiled successfully. I learned (or rather re-learned) something from this:
Variables declared as
dynamic
still have a fixed data type. Doing so simply means that the type is unknown at compile time, and effectively disables the type-checking system. This makes invalid type-casts more difficult to spot and disables Intellisense.
Luckily, there was a solution to allow both JObject
and ExpandoObject
data to be processed. After searching for a bit, I found that ExpandoObject
implements IDictionary<string, object>
. I also found that JObject
instances can be converted into a Dictionary<string, object>
, which implements IDictionary<string, object>
: a common type!
I gained two advantages by refactoring the service to work with IDictionary<string, object>
(and mapping the incoming data): I was able to use it with multiple input data types, and I could also benefit from compiler type checking.
The Summary
Declaring variables as dynamic
can make programming more complex. Firstly, doing so disables Intellisense on the respective variables. Secondly, the code becomes susceptible to throwing exceptions at runtime if the actual data types do not match; this can happen even when the code compiles successfully. Care should be taken when using multiple dynamic data types. Where possible, mapping to a common format can help to prevent errors.
Handling dynamically typed data is not very common in C#. There are legitimate use cases though, and there are mechanisms in the language should the need arise. These two incidents helped me to better understand these systems, and the potential pitfalls of using them. I hope this has been interesting and offered some insight into this topic; I also hope that it’s helpful if you’re ever in a similar situation.