Change in all things is sweet. - Aristotle
With all due respect to the great philosopher, he obviously never had to write software…
Head on over to Stack Overflow and search for “Immutable Object Patterns in C#” and you will see that this is a point of contention for many developers. Creating immutable objects in C# is cumbersome at best. What if I told you there is a practically effortless way to accomplish this with about one quarter of the code that is used in typical examples. I knew that would get your attention! Hang on tight while I walk you through an exciting demonstration!
As a side note, I’m not going to try to sell anyone on immutability in this post. There are countless other articles out there with that purpose. From here forward, I’m going to assume the reader understands why and when they need immutable objects.
The Problem
It’s a typical Friday night, when the phone rings. It’s NASA again (sigh…), and they need another brilliant celestial program in the key of C#. So much for relaxing, duty calls… After contemplating the specs, it’s obvious that an immutable planet class is a requirement. Let’s get started.
Typical C# Solution
In order to create the planet object in C#, you must create a constructor that accepts all property values and then sets those property values in the constructor body. This is a fairly recent addition to C#. In older versions you also had to create backing fields for each property. Additionally, it’s wise to override the equals method because by default C# uses reference equality. This could be a topic for debate and your mileage may vary; however, for the sake of argument let’s just assume we want value equality for planet objects. Another debatable topic may be whether to use a struct or a class. This example uses a class because C# will always create default parameterless constructors for structs. It doesn’t really make sense to have an instance of a planet that cannot change and is initialized without values. The result is the code below.
public class Planet
{
public Planet(
string name,
decimal massKg,
decimal equatorialDiameterKm,
decimal polarDiameterKm,
decimal equatorialCircumferenceKm,
decimal orbitalDistanceKm,
decimal orbitPeriodEarthDays,
decimal minSurfaceTemperatureCelsius,
decimal maxSurfaceTemperatureCelsius)
{
this.Name = name;
this.MassKg = massKg;
this.EquatorialDiameterKm = equatorialDiameterKm;
this.PolarDiameterKm = polarDiameterKm;
this.EquatorialCircumferenceKm = equatorialCircumferenceKm;
this.OrbitalDistanceKm = orbitalDistanceKm;
this.OrbitPeriodEarthDays = orbitPeriodEarthDays;
this.MinSurfaceTemperatureCelsius = minSurfaceTemperatureCelsius;
this.MaxSurfaceTemperatureCelsius = maxSurfaceTemperatureCelsius;
}
public string Name { get; }
public decimal MassKg { get; }
public decimal EquatorialDiameterKm { get; }
public decimal PolarDiameterKm { get; }
public decimal EquatorialCircumferenceKm { get; }
public decimal OrbitalDistanceKm { get; }
public decimal OrbitPeriodEarthDays { get; }
public decimal MinSurfaceTemperatureCelsius { get; }
public decimal MaxSurfaceTemperatureCelsius { get; }
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
{
return false;
}
if (ReferenceEquals(this, obj))
{
return true;
}
return obj.GetType() == this.GetType() && this.Equals((Planet)obj);
}
protected bool Equals(Planet other)
=>
string.Equals(this.Name, other.Name) && this.MassKg == other.MassKg
&& this.EquatorialDiameterKm == other.EquatorialDiameterKm
&& this.PolarDiameterKm == other.PolarDiameterKm
&& this.EquatorialCircumferenceKm == other.EquatorialCircumferenceKm
&& this.OrbitalDistanceKm == other.OrbitalDistanceKm
&& this.OrbitPeriodEarthDays == other.OrbitPeriodEarthDays
&& this.MinSurfaceTemperatureCelsius == other.MinSurfaceTemperatureCelsius
&& this.MaxSurfaceTemperatureCelsius == other.MaxSurfaceTemperatureCelsius;
public override int GetHashCode()
{
unchecked
{
var hashCode = (this.Name?.GetHashCode() ?? 0);
hashCode = (hashCode * 397) ^ this.MassKg.GetHashCode();
hashCode = (hashCode * 397) ^ this.EquatorialDiameterKm.GetHashCode();
hashCode = (hashCode * 397) ^ this.PolarDiameterKm.GetHashCode();
hashCode = (hashCode * 397) ^ this.EquatorialCircumferenceKm.GetHashCode();
hashCode = (hashCode * 397) ^ this.OrbitalDistanceKm.GetHashCode();
hashCode = (hashCode * 397) ^ this.OrbitPeriodEarthDays.GetHashCode();
hashCode = (hashCode * 397) ^ this.MinSurfaceTemperatureCelsius.GetHashCode();
hashCode = (hashCode * 397) ^ this.MaxSurfaceTemperatureCelsius.GetHashCode();
return hashCode;
}
}
}
Wow, that’s a lot of code. Looking ahead, let’s imagine what it will take to add a new property in the future. All the following changes are required:
- Constructor arguments
- Constructor body
- Properties
- Equals method
GetHashCode
method
That’s a ton of changes with several opportunities for error. All that typing! My fingers are sore!
A Better Way
Fortunately, there is a better way to create the planet class! The answer is, use F#. I know what you’re thinking, “Wait, hold the presses! Look at the title of this blog post!”. Hold on, allow me to elucidate. I’ll first put you at ease by telling you that you don’t even need to learn F# to use this solution. You just need to create a simple F# project and reference it from your C# project. Because F# is a dot net language, F# libraries are entirely accessible to C#.
F# has a construct known as a record type that mimics the behavior of the C# class shown above. The best part is that record types are effortless to define. Below is the code required to create a planet class that behaves identically to the C# class defined above.
type Planet = { Name: string; MassKg: decimal; EquatorialDiameterKm: decimal;
PolarDiameterKm: decimal; EquatorialCircumferenceKm: decimal;
OrbitalDistanceKm: decimal; OrbitPeriodEarthDays: decimal;
MinSurfaceTemperatureCelsius: decimal; MaxSurfaceTemperatureCelsius: decimal }
You don’t get much more concise and maintainable than that! Adding new properties is a relative breeze.
What I like most about this solution is that it acts as a “gateway drug”. There are many situations where F# offers significant advantages. However, many developers are intimidated by taking a big leap into a new language. How do you sell it to your boss? Where do you begin? This is a very pain free way to get your F# “foot in door”.
Conclusion
Using an F# library for immutable objects reduces required code by an amazing amount. Additionally, it eradicates error prone maintenance. With this new weapon in our arsenal, we can knock out that program for NASA and be done in time to curl up on the couch with a cigar and scotch before bed. Yay for humanity!
If you need more concrete examples, I encourage you to go have a look at the accompanying github repository. Download the source code, and notice that there are 4 projects. A C# domain project, an F# domain project, a C# planet repository project, and a test project. Change the reference in the planet repository project back and forth from the C# and F# projects in order to demonstrate the solution.
I hope this solution saves you as much time and trouble as it has me. As always, thank you for reading and feel free to contact me with any questions.