James Randall Musings on software development, business and technology.
C# / Blazor Wolfenstein - Part 3 - Records and Collections
Code

I was in the bath, with my rubber duck of course, thinking about the collection types to use with my record based models. One of the key features of records, at least in F#, is that they support value equality comparisons and this applies deeply. For example:

type ExampleRecord = { Title: string ;  Numbers: int list }
let record1 = { Title = "Hello world" ; Numbers = [ 4 ; 3 ] }
let record2 = { Title = "Hello world" ; Numbers = [ 4 ; 3 ] }
let record3 = { Title = "Hello world" ; Numbers = [ 4 ; 1 ] }
let record4 = { Title = "Hello" ; Numbers = [ 4 ; 3 ] }

record1 = record2 // true, they contain the same values
record1 = record3 // false - the collection is different
record1 = record4 // false - the title is different

Records in C# share this behaviour: For records, value equality means that two variables of a record type are equal if the types match and all property and field values match. For other reference types such as classes, equality means reference equality. That is, two variables of a class type are equal if they refer to the same object. Methods and operators that determine equality of two record instances use value equality.

There’s a subtlety in this that isn’t pointed out explicitly but which is very easy to stumble across in C#: if records contain properties that are themselves classes then those properties will be compared using reference equality rather than value equality (unless the classes specifically implement equality). And the gotcha you are likely to hit on basic models is collections. C# collections do not do deep equality. And so in C# the example above will yield different results:

public record ExampleRecord(string Title, int[] Numbers);
var record1 = new ExampleRecord("Hello World", new[] { 4, 3 });
var record2 = new ExampleRecord("Hello World", new[] { 4, 3 });
var record3 = new ExampleRecord("Hello World", new[] { 4, 1 });
var record4 = new ExampleRecord("Title", new[] { 4, 3 });
System.Console.WriteLine(record1 == record2); // false 
System.Console.WriteLine(record1 == record3); // false
System.Console.WriteLine(record1 == record4); // false

Because of the presence of the collection, which makes use of reference equality, all three comparisons return false. To demonstrate this consider the following:

public record ExampleRecord(string Title, int[] Numbers);
var commonCollection = new[] {4, 3};
var record1 = new ExampleRecord("Hello World", commonCollection);
var record2 = new ExampleRecord("Hello World", commonCollection);
System.Console.WriteLine(record1 == record2); // true

In this case the comparison succeeds because both records reference the same collection.

Its worth noting this is also the case in F# which we can see by using a C# collection instead of the F# list:

type ExampleRecord = { Title: string ;  Numbers: System.Collections.Generic.List<int> }
let record1 = { Title = "Hello world" ; Numbers = new System.Collections.Generic.List<int>([ 4 ; 3 ]) }
let record2 = { Title = "Hello world" ; Numbers = new System.Collections.Generic.List<int>([ 4 ; 3 ]) }
record1 = record2 // false as the collections are compared by reference and not by deep value

However you are far more likely to stumble into this little gotcha in C# than you are in F# due to its history, original design goals, and that 20+ years worth of code is class based and designed for reference equality. As far as I am aware there are no collections in the .NET framework that support value equality but you can roll your own by deriving new collection types and implementing your own equality methods. Though at some point you may still get caught out by a class inside a record.

Worth noting I don’t think it would make any sense for the .NET team to change the behaviour of existing collections. It would be carnage and change the behaviour of 20+ years worth of code. And performing deep equality checks can be expensive. Might be nice to see some value equality based immutable collections appear in the framework at some point but I guess they’d argue clutter.

In any case from a Wolfenstein perspective I don’t think this is an issue. I don’t recall relying on any deep value equality behaviour but needless to say if I do I’ll be blogging again with collections (or preferably some that someone else has already written!). My bath time thinking mostly revolved around immutability, this was more one of those “oh, of course” side thoughts.

If you want to discuss this or have any questions then the best place is on GitHub. I’m only really using Twitter to post updates to my blog these days.