C# 9.0 — Record Compatible Collections
In the latest version of the language, an alternative to class
was introduced:
Records are immutable, making them ideal to represent objects such as those exposed through your API, or any other simple object that contains only data and minimal or no logic: from a functional standpoint, you’ll likely end up with cleaner code when these objects are immutable, but prior to record types, enforcing this was generally more work than it was worth.
Since records are intended to represent only data, equality is value based:
// false if Blog is a class, true if Blog is a record
new Blog { Id = 1 } == new Blog { Id = 1 };
The gotcha here will be when your record contains properties that are reference types: ideally, your properties would only contain value types or other records, but what do you do when your record contains a collection?
Since even basic arrays are not evaluated by the value of the contents, we need to find an enumerable type that is. Since records are immutable, ideally this enumerable type should be readonly as well.
Unfortunately, a built in collection type meeting these requirements does not appear to exist, but good news everyone! We can easily create one:
The class is simply a wrapper for an Array that will evaluate equality by the contents for the array. I’ve included an implicit operator
that will allow you to assign arrays, or call ToArray()
on an enumerable, and have it converted implicitly (i.e. RecordArray<int> foo = new[] { 1, 2 };
).
The sample code also includes a JsonConverterFactory
: by using a custom readonly collection, you’ll likely break the JSON serialization if these records are exposed through your API: this sample factory reuses the existing ArrayConverter
functionality, but determining an easy way to make that happen took way longer than I’d like to admit, and is the primary reason I created this blog post.
You may never run into this issue if you don’t compare objects in this manner; I only ran into it when my unit tests using FluentExpressions
started failing, since records override Equals()
and the tests used that opposed to comparing by member as it had been doing.
If you do run into this issue, hopefully this sample code will be a good starting point for a solution.