Record Data Type And Collections In C# 9.0

Record data type and Collections in C#

Hello everyone, this time, I came up with a small topic related to using Record data types in C# 9.0 together with collections.
What we realized, C# 9.0 gives us a data structure perfectly suitable for Immutable Objects.
Immutable Objects are not new; they exist in Java, C#, Python, TypeScript, React.JS, and many other languages and frameworks.
Why people use Immutable Objects is clear, they provide some significant advantages in a particular context:

  • Eliminate unexpected side effects in behavior when working with shared objects, including shared among threads;
  • Maintain a consistent state.

Perhaps the main disadvantages of using Immutable Objects are:

  • In performance-critical systems, Immutable Objects may lead to higher data allocation.
  • Require more coding for equality comparers and property modifications instantiating new objects.

If you use Immutable Objects in C# and see it helpful in your case, the new Record data type should help with the last point of the disadvantages list by reducing the amount of code you should typically write.
Let’s take a concrete example with a simple blog post model and see how a typical class, not record, would look.

public class Post
{
    public string Body { get; }

    public Post(string body)
    {
        Body = body;
    }

    protected bool Equals(Post other)
    {
        return Body == other.Body;
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((Post) obj);
    }

    public override int GetHashCode()
    {
        return (Body != null ? Body.GetHashCode() : 0);
    }
}

As we see from the code above, it’s quite an overhead to generate one property and compare objects by their content with equality members’ help.

With C# 9.0 and the new Records data type, the equality members are not necessary anymore because the compiler does that job for us automatically. Let’s see how an equivalent code looks like:

public record Post
{
    public string Body { get; }
    public Post(string body) => Body = body;
}

t looks significantly simpler, cleaner and provides value comparison logic out of the box. Just have a look at the disassembled code, and you’ll see that the .net compiler generates equality members for us behind the scene.

Adding a new property is fairly easy, no need to bother about a huge boilerplate of code.

At first glance, it’s all good unless you start working with collections. Once you add a property, say, a collection of tags, assigned to the post:
public ReadOnlyCollection<string> Tags { get; }

It stops working out of the box as ReadOnlyCollection or ImmutableList – whatever you prefer – is a reference type and is compared accordingly by reference, not values. As of now, there is no data type you could use directly from the framework.

A pretty simple solution exists though, you might want to create a wrapper around ReadOnlyCollection and implement equality members as you wish.

public class RecordsCollection<T>: ReadOnlyCollection<T>
{
    public RecordsCollection(params T[] values) : base(values == null
        ? new List<T>()
        : new List<T>(values))
    {
    }

    public RecordsCollection(IList<T> list) : base(list)
    {
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj))
        {
            return false;
        }

        if (ReferenceEquals(this, obj))
        {
            return true;
        }

        return obj is RecordsCollection<T> collection &&
               collection.Items.SequenceEqual(Items);
    }


    public override int GetHashCode()
    {
        var hashCode = new HashCode();
        foreach (T item in Items)
        {
            hashCode.Add(item);
        }

        return hashCode.ToHashCode();
    }
}

In this example, equality is defined by the sequence of the elements. Therefore the final Post record would work as expected and look simple and clear as it should:

public record Post
{
    public string Body { get; }
    public RecordsCollection<string>  Tags { get; }
    
    public Post(string body, RecordsCollection<string> tags)
        => (Body, Tags) = (body, tags);
}

To see the code examples and some Unit Tests covering certain scenarios, just follow this link

If you have some thoughts, suggestions, or spotted a mistake, please drop me a message.

I wish you a pleasant experience while writing a code with C# 9.0.

Take care,
Ievgen