From C# Arithmetic Expressions to JSON and Back
The previous blog post has introduced a calculator engine project. This post extends it to convert arithmetic expressions to and from JSON.
For example, an expression built like this:
Expression.CreateMultiValued([3, 3], new Addition())
could be represented in JSON as:
{
"Operation": "Addition",
"Subexpressions": [
{
"Value": 3
},
{
"Value": 3
}
]
}
The objective is to implement an ExpressionJsonSerializer
class with the following interface that does such conversions:
public interface IExpressionJsonSerializer
{
public string Serialize(Expression expression);
public Expression? Deserialize(string json);
}
Terminology
Serialisation is the process of converting a C# object into a format that can be stored or transmitted. Once converted, it can be written to a file, stored in a database, or sent over a network.
Deserialisation is the reverse process of converting serialised data back into its object form.
JSON (JavaScript Object Notation) is one among many suitable for serialisation data formats.
Map and Convert are more general terms than Serialise & Deserialise. They can refer to anything that becomes something else.
Plan
The objective is to implement an ExpressionJsonSerializer
class with the following interface:
public interface IExpressionJsonSerializer
{
public string Serialize(Expression expression);
public Expression? Deserialize(string json);
}
- The serialiser class uses US spelling. The reason for this is to maintain consistency with the
System.Text.Json
library and to adhere with the default IDE spellcheck suggestions. - The
Deserialize
method returns a nullableExpression
type. This is because some strings do not represent a valid expression. Expression-convertible strings get mapped to their correspondingExpression
objects, while expression-nonconvertible strings get mapped tonull
.
The serialiser requires validation of its correctness. One way to validate it is through unit tests. One way to set up unit tests is the following three-step process:
- Create one expression by deserialisation.
- Create another expression by using static builder methods that the
Expression
class already implements. - Compare that the two expressions are equal.
The last step requires the Expression
class to implement equality comparison.
This leads to a three-step plan: implement equality comparison, set up tests, implement the serialiser.
Implementation
This section details the commit-by-commit implementation in 5 steps.
Step 0: Branching
This step sets up a dedicated git branch for developing this solution. Assuming that the current branch is main
, the command git pull
makes the local code up to date with remote. And the command git checkokut -b feature/json
creates a new branch named feature/json
and switches to it.
Step 1: Value object
The two types of equality comparisons in C# are reference equality and value equality.
- Reference equality checks whether two objects point to the same location in memory. It is the default equality that C# implements for all reference types.
- Value equality checks whether two objects store the same values.
This solution requires value equality because expressions should be considered equal regardless of the way they are built.
One way to implement value equality is to have an abstract value object class that implements comparison logic and have derived classes pass on a stream of their custom equality components.
This commit borrows the value objects implementation from here. And the next commit customises the implementation to this solution.
Step 2: Operations comparison
This step implements value equality for operations. It does so in two parts. The first commit sets up the tests to check value equality comparison for operations. At this point half of the tests are failing. The next commit provides implementation that passes all the tests.
There are two key points.
- The
Operation
class does not have any equality components.Nonetheless, different subclasses are correctly compared as non-equal because theValueObject
class also compares the object types. - The
Constant
class gets the base equality components in addition to its own. In this case including it is unnecessary because theOperation
class does not have any equality components; however, it is necessary in general.
Step 3: Expressions comparison
This step implements value equality for expressions. Two expressions are equal when they have the same operation and their subexpressions are equal.
The implementation proceeds in the same way as for operations: the first commit sets up the tests, the second commit provides implementation, and the third commit does some refactoring.
Step 4: Serialiser
This step implements the serialiser.
The first commit sets up the interface and tests. Since no implementation is provided yet, all tests are failing. It also includes the following class that acts as an intermediate model between json strings and expressions:
private sealed class ExpressionModel
{
public double? Value { get; set; }
public string? Operation { get; set; }
public List<ExpressionModel>? Subexpressions { get; set; }
}
The second commit implements deserialisation.
The third commit implements serialisation.
There are 4 key points.
- The serialiser uses
System.Text.Json
library to convert betweenExpressionModel
and json strings. - The
Operation
class enforces all subclasses to have a name by using public abstract string propertyName
. - The
Operation
class implements a static builder methodFromName
to create operations by name. - The
Expression
class exposes its private fields as readonly public properties.
Conclusion
Expressions can now be compared to each other and converted to and from JSON.
JSON stands for ...
JavaScript Object Notation. Reference: https://en.wikipedia.org/wiki/JSONThe two types of equality comparisons in C# are ...
Reference equality & Value equality. Reference: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/equality-comparisonsGit CLI command to create a new branch X and switch to it is ...
git checkout -b X
. Reference: https://www.git-scm.com/docs/git-checkout#Documentation/git-checkout.txt-emgitcheckoutem-b-Bltnew-branchgtltstart-pointgtThe way that this solution implements value equality is ...
Deriving from ValueObject class and overriding GetEqualityComponents method.The serialisation library that this solution uses is ...
System.Text.Json. Reference: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overviewJsonSerializerOptions to ignore nulls is ...
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
. Reference: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/ignore-properties#ignore-all-null-value-properties