SHARE:

Introducing C# 9: Records

Introduction

C# 9 introduces Init-only properties that allow to make individual properties immutable. C# 9 introduces another great feature that enable a whole object to be immutable and make it acting like a value: Records. Let’s see in this article how Records work. Unlike the previous announcement from Microsoft (https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/), data class keywords become now record keyword.

Record keyword

C# 9 Introduces a new keyword: record keyword. record keyword makes an object immutable and behave like a value type. To make the whole object immutable you have to set init keyword on each property if you are using an implicit parameterless constructor:

Example:

namespace CSharp9Demo.Models
{
public record Product
{
public string Name { get; init; }
public int CategoryId { get; init; }
}
}

With-expressions

We might want sometimes create new a object from another one because some property values are identical only one change, unfortunately your object is immutable. with keyword fixes that. It allows you create an object from another by specifying what property changes:

using System;
using CSharp9Demo.Models
namespace CSharp9Demo
{
class Program
{
static void Main(string[] args)
{
var product = new Product
{
Name = "VideoGame",
CategoryId = 1
};
var newProduct = product with { CategoryId = 2 }
// newProduct.Name == "VideoGame"
// newProduct.CategoryId == 2
}
}
}

Objects comparison

On that point Records work like Structs, these last override the virtual Equals method to enable value-based comparison, which means each property will be compared with a value-based approach. You already have probably noticed that ReferenceEquals method won’t work (always false), because it compares two same objects (reference-based comparison).

Example:

using System;
using CSharp9Demo.Models
namespace CSharp9Demo
{
class Program
{
static void Main(string[] args)
{
var product = new Product
{
Name = "VideoGame",
CategoryId = 1
};
var newProduct = product with { CategoryId = 2 }
product.Equals(newProduct); // returns false
product == newProduct; // returns false
var newAnotherProduct = new Product
{
Name = "VideoGame",
CategoryId = 1
};
product.Equals(newAnotherProduct); // returns true
product == newAnotherProduct; // returns false
}
}
}

Positional records

Constructors and deconstructors are allowed in Records. That’s good ! 🙂

Example:

using System;
namespace CSharp9Demo.Models
{
public record Product
{
public string Name { get; init; }
public int CategoryId { get; init; }
public Product(string name, int categoryId)
=> (Name, CategoryId) = (name, categoryId);
public void Deconstruct(out string name, out int categoryId)
=> (name, categoryId) = (Name, CategoryId);
}
}

But C# 9 brings a shorter syntax (Records only) named Positional Records, that allows a shorter syntax by a specific position of members:

using System;
namespace CSharp9Demo.Models
{
public record Product(string Name, int CategoryId);
}

As you may noticed, this very short syntax makes Name and CategoryId public init-only auto-properties, in other words, this “one line syntax”, makes the record immutable, and their value assignment is determined by their position. Construction (by position) and deconstruction (by position) will work fine with that syntax you already know with previous C# releases.

using System;
using CSharp9Demo.Models
namespace CSharp9Demo
{
class Program
{
static void Main(string[] args)
{
var product = new Product("VideoGame", 1);
var (name, categoryId) = product;
}
}
}

With-expressions and inheritance

First, inheritance is definitely supported by records.

Secondly, Records hide a clone method thats copy the whole object, then, with with expression, if you store a child object to a parent object variable, the type and the content will be preserved:

Example of a Book class that inherits from Product class:

using System;
namespace CSharp9Demo.Models
{
public record Product
{
public string Name { get; init; }
public int CategoryId { get; init; }
}
public record Book : Product
{
public string ISBN { get; init; }
}
}
view raw Book.cs hosted with ❤ by GitHub

Here is now what happen during after the assignment:

using System;
using CSharp9Demo.Models
namespace CSharp9Demo
{
class Program
{
static void Main(string[] args)
{
Product product = new Book
{
Name = "VideoGame",
CategoryId = 1,
ISBN = "00000000000000"
};
var newProduct = product with { CategoryId = 2 }
// newProduct.GetType().Name == Book
// newProduct.Name == "VideoGame"
// newProduct.CategoryId == 2
// newProduct.ISBN == "00000000000000"
var book = (Book)newProduct;
book.ISBN; //available after casting
}
}
}

newProduct is finally a book: Microsoft gave the explanation: “Records have a hidden virtual method that is entrusted with “cloning” the whole object. Every derived record type overrides this method to call the copy constructor of that type, and the copy constructor of a derived record chains to the copy constructor of the base record. A with expression simply calls the hidden “clone” method and applies the object initializer to the result.” Source here.

Value based comparison and inheritance

C# 9 Records introduce EqualityContract. Records have a virtual protected property named EqualityContract (and every derived record overrides it) to ensure that two differents kind of objects are comparable in the same way whatever which object is compared to another one regarding the order. Example:

using System;
using CSharp9Demo.Models
namespace CSharp9Demo
{
class Program
{
static void Main(string[] args)
{
Product product1 = new Book
{
Name = "VideoGame",
CategoryId = 1,
ISBN = "00000000000000"
};
Product product2 = new Product
{
Name = "VideoGame",
CategoryId = 1
};
product1.Equals(product2); // Should return false
product2.Equals(product1); // Should also return false
}
}
}

product2 “might think” product1 is the same because it will compare shared properties (Name and CategoryId), but product1 might think product2 is different because there is a missing property (ISBN).

EqualityContract is there to “arbitrate a consensus”, then product1.Equals(product2) and product2.Equals(product1) must both return false;

Written by

anthonygiretti

Anthony is a specialist in Web technologies (14 years of experience), in particular Microsoft .NET and learns the Cloud Azure platform. He has received twice the Microsoft MVP award and he is also certified Microsoft MCSD and Azure Fundamentals.