Immutable Objects in C#
You may have heard about the benefits of immutable objects, especially from the functional programming crowd. But creating and working with immutable objects in a language like C# can be tricky.
Traditional approach
The usual way to do this is to create a constructor that takes all the data and assigns it to read-only properties. Luckily, making our properties truly read-only is now easier in C# 6 and no longer requires a backing field. So we start with something like this:
public class Person
{
public string FirstName { get; }
public string LastName { get; }
public string Email { get; }
public Person(string firstName, string lastName, string email)
{
FirstName = firstName;
LastName = lastName;
Email = email;
}
}
Then, when we need to change a property, we create a new object, passing in the current values for things that aren’t changing:
var person = new Person("first", "last", "email");
var updatedEmail = new Person(person.FirstName, person.LastName, "newEmail");
This works, but as you can imagine, it quickly becomes unwieldy as the number of properties grows.
In the end, it’s a lot of code to write and maintain, and it’s clunky to create new objects from existing ones. Our intentions are good, but the language certainly isn’t helping us much.
Enter F#
What we really want is something like F# records. Those give us immutability for free, along with a helpful feature called Copy and Update Expressions:
type Person = {
FirstName: string
LastName: string
Email: string
}
let person = { FirstName = "first"; LastName = "last"; Email = "email" }
let updatedEmail = { person with Email = "newEmail" } // Copy and Update Expression
Nice, right? updatedEmail
gets all the properties of person
, but with a new value for Email
.
C# + F# = ?
Here’s a crazy idea: what if we were to write our domain object in F# and use it in the rest of our C# code? Since both languages target the CLR, interop is pretty easy. The only requirement is that the F# code has to live in a separate assembly.
Let’s try it out. On the F# side, we have:
type Person = {
FirstName: string
LastName: string
Email: string
}
with member this.WithUpdatedEmail email = { this with Email = email }
The with member
syntax attaches a method to our record called WithUpdatedEmail
, which takes in a new email and uses the copy and update syntax to create a new object. Here’s how we use it on the C# side:
var person = new Person("first", "last", "email");
var updatedEmail = person.WithUpdatedEmail("newEmail");
Conclusion
That was a fun little experiment, and hopefully it gives you a taste of some things you can do. It’s always good to look around the corner and see how other languages are doing things. It may not be feasible to rewrite an entire C# codebase in F#, but you can always incorporate ideas from F#, and perhaps a bit of code as well!