Joydip Kanjilal
Contributor

How to work with record types in C# 9

how-to
Feb 15, 20218 mins
C#Microsoft .NETSoftware Development

Take advantage of record types in C# 9 to build immutable types and thread-safe objects.

Binary record player grooves
Credit: Thinkstock

Immutability makes your objects thread-safe and helps improve memory management. It also makes your code more readable and easier to maintain. An immutable object is defined as an object that, once created, can’t change. Thus an immutable object is inherently thread-safe and immune from race-conditions.

Until recently, C# did not support immutability out-of-the-box. C# 9 introduces support for immutability with new init-only properties and record types. Init-only properties can be used to make the individual properties of an object immutable, and records can be used to make the entire object immutable.

Because immutable objects don’t change their state, immutability is a desirable feature in many use cases such as multi-threading and Data Transfer Objects. This article discusses how we can use init-only properties and record types in C# 9.

To work with the code examples provided in this article, you should have Visual Studio 2019 installed in your system. If you don’t already have a copy, you can download Visual Studio 2019 here.

Create a console application project in Visual Studio

First off, let’s create a .NET Core console application project in Visual Studio. Assuming Visual Studio 2019 is installed in your system, follow the steps outlined below to create a new .NET Core console application project in Visual Studio.

  1. Launch the Visual Studio IDE.
  2. Click on “Create new project.”
  3. In the “Create new project” window, select “Console App (.NET Core)” from the list of templates displayed.
  4. Click Next.
  5. In the “Configure your new project” window shown next, specify the name and location for the new project.
  6. Click Create.

Following these steps will create a new .NET Core console application project in Visual Studio 2019. We’ll use this project in the subsequent sections of this article.

Use init-only properties in C# 9

Init-only properties are those that can be assigned values only at the time when the object is initialized. Refer to the following class that contains init-only properties.

public class DbMetadata
    {
        public string DbName { get; init; }
        public string DbType { get; init; }
    }

You can use the following code snippet to create an instance of the DbMetadata class and initialize its properties.

DbMetadata dbMetadata = new DbMetadata()
{
      DbName = "Test",
      DbType = "Oracle"
};

Note that subsequent assignments to the init-only fields are illegal. Hence the following statement will not compile.

dbMetadata.DbType = "SQL Server";

Use record types in C# 9

A record type in C# 9 is a lightweight, immutable data type (or a lightweight class) that has read-only properties only. Because a record type is immutable, it is thread-safe and cannot mutate or change after it has been created. You can initialize a record type only inside a constructor.

You can declare a record using the record keyword as shown in the code snippet below.

public record Person
{
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string Address { get; set; }
  public string City { get; set; }
  public string Country { get; set; }
}

Note that merely marking a type as a record (as shown in the preceding code snippet) will not give you immutability on its own. To give immutability to your record type you must use init properties as shown in the code snippet below.

public record Person
{
  public string FirstName { get; init; }
  public string LastName { get; init; }
  public string Address { get; init; }
  public string City { get; init; }
  public string Country { get; init; }
}

You can use the following code snippet to create an instance of the Person class and initialize its properties.

var person = new Person
{
  FirstName = "Joydip",
  LastName = "Kanjilal",
  Address = "192/79 Stafford Hills",
  City = "Hyderabad",
  Country = "India"
};

Use with-expressions in C# 9

You might often want to create an object from another object, if some of the properties have identical values. However, the init-only properties of a record type will prevent this. For example, the following code snippet will not compile because all of the properties of the record type named Person are init-only by default.

var newPerson = person;
newPerson.Address = "112 Stafford Hills";
newPerson.City = "Bangalore";

Fortunately there is a workaround—the with keyword. You can take advantage of the with keyword to create an instance of a record type from another by specifying the changes in the values of the properties. The following code snippet illustrates how this can be accomplished.

var newPerson = person with
            { Address = "112 Stafford Hills", City = "Bangalore" };

Inheritance in record types in C# 9

Record types support inheritance. That is, you can create a new record type from an existing record type and add new properties. The following code snippet illustrates how you can create a new record type by extending an existing one.

public record Employee : Person
{
   public int Id { get; init; }
   public double Salary { get; init; }
}

Positional records in C# 9

Instances of record types that are created using positional arguments are immutable by default. In other words, you can create an immutable instance of a record type by passing an ordered list of parameters using constructor arguments as shown in the code snippet given below.

var person = new Person("Joydip", "Kanjilal", "192/79 Stafford Hills", "Hyderabad", "India");

Check record instances for equality in C# 9

When you check for equality of two instances of a class in C#, the comparison is based on the references of those objects (identity). However, if you check for equality of two instances of a record type, the comparison is based on the values within the instances of the record type.

The following code snippet illustrates a record type named DbMetadata that consists of two string properties.

public record DbMetadata
{
   public string DbName { get; init; }
   public string DbType { get; init; }
}

The following code snippet shows how you can create two instances of the DbMetadata record type.

DbMetadata dbMetadata1 = new DbMetadata()
{
   DbName = "Test",
   DbType = "Oracle"
};
DbMetadata dbMetadata2 = new DbMetadata()
{
   DbName = "Test",
   DbType = "SQL Server"
};

You can check for equality using the Equals method. The following two statements will display “false” at the console window.

Console.WriteLine(dbMetadata1.Equals(dbMetadata2));
Console.WriteLine(dbMetadata2.Equals(dbMetadata1));

Consider the following code snippet that creates a third instance of the DbMetadata record type. Note that the instances dbMetadata1 and dbMetadata3 contain identical values.

DbMetadata dbMetadata3 = new DbMetadata()
{
   DbName = "Test",
   DbType = "Oracle"
};

The following two statements will display “true” at the console window.

Console.WriteLine(dbMetadata1.Equals(dbMetadata3));
Console.WriteLine(dbMetadata3.Equals(dbMetadata1));

Although a record type is a reference type, C# 9 provides synthetic methods to follow value-based equality semantics. The compiler generates the following methods for your record type to enforce value-based semantics:

  • An override of Object.Equals(Object) method
  • A virtual Equals method that accepts a record type as its parameter
  • An override of the Object.GetHashCode() method
  • Methods for the two equality operators, namely operator == and operator !=
  • Record types implement System.IEquatable

Additionally, record types provide an override of the Object.ToString() method. These methods are generated implicitly and you need not re-implement them.

Check for Equals method in C#

You can check to see if the Equals method has been generated implicitly. To do this, add an Equals method in the DbMetadata record as shown below.

 public record DbMetadata
    {
        public string DbName { get; init; }
        public string DbType { get; init; }
        public override bool Equals(object obj) =>
        obj is DbMetadata dbMetadata && Equals(dbMetadata);
    }

When you compile your code, the compiler will flag an error with the following message:

Type ‘DbMetadata’ already defines a member called ‘Equals’ with the same parameter types

Although a record type is a class, the record keyword provides additional value-like behaviors and semantics that make a record different from a class. A record is itself a reference type but it uses its own built-in equality check — the equality is checked by value and not reference. Finally, note that records can be mutable but they are primarily designed for immutability.

How to do more in C#:

Joydip Kanjilal
Contributor

Joydip Kanjilal is a Microsoft Most Valuable Professional (MVP) in ASP.NET, as well as a speaker and the author of several books and articles. He received the prestigious MVP award for 2007, 2008, 2009, 2010, 2011, and 2012.

He has more than 20 years of experience in IT, with more than 16 years in Microsoft .Net and related technologies. He has been selected as MSDN Featured Developer of the Fortnight (MSDN) and as Community Credit Winner several times.

He is the author of eight books and more than 500 articles. Many of his articles have been featured at Microsoft’s Official Site on ASP.Net.

He was a speaker at the Spark IT 2010 event and at the Dr. Dobb’s Conference 2014 in Bangalore. He has also worked as a judge for the Jolt Awards at Dr. Dobb's Journal. He is a regular speaker at the SSWUG Virtual Conference, which is held twice each year.

More from this author