4 minute read

When I should consider using record?

You want:

  • reference type, but the comparison should be based on values of properties
  • immutable entity. Usage of immutable entities is widely recommended as it increases the application’s behavior predictability and reduces the number of bugs and issues
  • support for inheritance hierarchies

TLDR: you want to store immutable data, preserving the ability to pass instances by reference.

How to use it?

record type is available starting with .NET 5 and C# 9.0.

Declaring records records

You can declare record using positional constructor:

public record Car(string Manufacturer, string Model, decimal Price);

or more traditional syntax:

public record Vehicle()
{
    public string Manufacturer { get; init; } // technically, you can
    public string Model { get; init; } // declare it as { get;set; }
    public decimal Price { get; init; } // but don't do it :)
}

or you can declare standard properties in addition to positionals:

public record CarWithMileage(string Manufacturer, string Model, decimal Price)
{
    public int Mileage { get; init; }
}

Using records

// create new instance
var car = new Car("Tesla", "Model S", 50000.00m);
// you can do like this
var otherCar = new Car(Manufacturer: "Ford", Model: "Explorer", Price: 60000.0m);

or like this, if you don’t have positional properties:

Vehicle another = new Vehicle()
{
    Manufacturer = "Toyota",
    Model = "Camry",
    Price = 25000.0m
};

or like this if you have positional and standard properties:

var carWithMileage = new CarWithMileage("VAZ", "2109", 100.0m)
{
    Mileage = 340000
};

Record comparison

records are compared by values:

var car = new Car("Tesla", "Model S", 50000.00m);
var sameCar = new Car("Tesla", "Model S", 50000.00m);
Console.WriteLine(car == sameCar); // true

same picture meme

For comparison repeat this with class type:

public class ClassCar
{
    public ClassCar(string manufacturer, string model, decimal price)
    {
        Manufacturer = manufacturer;
        Model = model;
        Price = price;
    }
    public string Manufacturer { get; init; }
    public string Model { get; init; }
    public decimal Price { get; init; }
}

// ...

var classCar = new ClassCar("Tesla", "Model S", 50000.00m);
var sameClassCar = new ClassCar("Tesla", "Model S", 50000.00m);
Console.WriteLine(classCar == sameClassCar); // false

Mutating records (or editing them)

So, how do you mutate the immutable?

mutate the immutable

You should use with expression to make a new record instance based on existing record with some modifications - this is called nondestructive mutation.

var car = new Car("Tesla", "Model S", 50000.0m);
var carWithDiscount = car with { Price = 45000.0m };
Console.WriteLine(car); // Car { Manufacturer = Tesla, Model = Model S, Price = 50000.0 }
Console.WriteLine(carWithDiscount); // Car { Manufacturer = Tesla, Model = Model S, Price = 45000.0 }

Note: with makes a shallow copy - reference properties will reference the same object as they were in original record.

JavaScript users use spread syntax to achieve the same thing:

const updatedObject = { ...oldObject, someProperty: 'new value' }

Nice features

Record inheritance

Just works. But only for records - you can’t inherit class. If you want to compare records both instances should be of the same run-time type or they will be considered non-equal without comparing their properties.

ToString() Formatting

ToString method returns human readable data:

Car { Manufacturer = Tesla, Model = Model S, Price = 50000.0 }

You can customize this by providing own PrintMembers method (see here).

Deconstruction

Have you ever envied JavaScript programmers because they could do this?

const user = {
  id: 42,
  is_verified: true,
}

const { id, is_verified } = user // destructuring

console.log(id) // 42
console.log(is_verified) // true

Example from Mozilla

Now you can do this:

var (manufacturer, model, price) = car;

But this only works with positional properties - for standard properties you will need to provide Deconstruct method (see here):

public void Deconstruct(out string something, out string otherthing)

Performance

records value equality works measurably faster than struct’s one.

How to choose between class, struct, record (class vs record)

Use

  • class - if you want reference types, supporting hierarchies and focusing on class’ responsibility and behavoir
  • record - if you want immutable reference types, supporting hierarchies and focusing on storing data
  • struct - if you want just to store some data and don’t need anything above

Records and Entity Framework Core

With EFCore you will need some additional actions to update a record:

var report = dbContext.Set<MyReport>().AsNoTracking().First(...some linq query...);
var updatedReport = report with {ReadyToUse = true};
dbContext.Update(updatedRecord);
dbContext.SaveChanges();

via RenierGlez@StackOverflow.

How does it work?

Compiler generates class and augments it with some synthesized methods and overrides:

  • Object.Equals(Object)
  • virtual Equals(T)
  • Object.GetHashCode()
  • operator== and operator!=
  • implements System.IEquatable<T>
  • ToString()

For with expression compiler generates a clone method and a copy constructor.

Here are some IL code for Car record:

// so it's a class
.class /* 02000006 */ public auto ansi beforefieldinit RecordExample.Car
	extends [System.Runtime]System.Object
	implements class [System.Runtime]System.IEquatable`1<class RecordExample.Car>

// ...
// ToString implementation
.method /* 0600000F */ public hidebysig virtual
		instance string ToString () cil managed
	{
// ...
// Clone method
.method /* 06000016 */ public hidebysig newslot virtual
		instance class RecordExample.Car '<Clone>$' () cil managed