Records

C# · Practical · 10 min

Records are the idiomatic way to model DTOs, messages and value objects with almost no code.

A record is a class (or struct) designed for data. The positional form declares everything in one line:

public record Person(string Name, int Age);

The compiler generates init-only properties, a constructor, Deconstruct, a readable ToString() (Person { Name = Ada, Age = 36 }), and value equality: two records are equal when their type and all properties are equal, so == compares contents rather than references.

Records encourage immutability. To "change" one, create a modified copy with a with-expression: var older = p with { Age = 37 };. The original is untouched. The copy is shallow: a List property is shared between both.

Variants:

  • record (or record class) is a reference type.
  • record struct is a value type with the same conveniences; readonly record struct makes it immutable.
  • You can add methods, extra properties and validation in a body, and records support inheritance from other records.

Because equality uses all properties, records work well as dictionary keys and in sets, and make tests easy to write (Assert.Equal(expected, actual)).

Use records for DTOs, API request/response models, events and value objects. Use regular classes for entities with identity and mutable lifecycle, such as EF Core entities.

Example

var a = new Person("Ada", 36);
var b = new Person("Ada", 36);
Console.WriteLine(a == b);                 // True (value equality)
Console.WriteLine(ReferenceEquals(a, b));  // False
Console.WriteLine(a);                      // Person { Name = Ada, Age = 36 }

var older = a with { Age = 37 };
Console.WriteLine($"{a.Age} {older.Age}"); // 36 37

var (name, age) = older;
Console.WriteLine($"{name} is {age}");     // Ada is 37

var seen = new HashSet<Point> { new(1, 2), new(1, 2) };
Console.WriteLine(seen.Count);             // 1

record Person(string Name, int Age);
readonly record struct Point(int X, int Y);

Remember

  • Positional records generate properties, constructor, ToString and Deconstruct
  • Records compare by value, classes by reference
  • with creates a modified shallow copy
  • record struct gives the same conveniences as a value type

Watch out for

  • Assuming with deep-copies collections inside the record; it only copies references.

Check yourself

  1. For record R(List<int> Xs), var r2 = r1 with { }; r2.Xs.Add(1); what happens to r1.Xs?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record