Site

Records — C# Record Types

Tutorial S10  •  C# / Learn

S10.0 What This Teaches

This tutorial covers C# record types for immutable data modeling:

S10.1 Record Types

A record is a reference type where equality is based on property values, not object identity. The compiler generates Equals, GetHashCode, ToString, and ==:
// Records - value equality on reference types.

record Person(string Name, int Age);

var alice1 = new Person("Alice", 30);
var alice2 = new Person("Alice", 30);
var bob    = new Person("Bob",   25);

Console.WriteLine(alice1 == alice2);   // True  - same values
Console.WriteLine(alice1 == bob);      // False
Console.WriteLine(alice1);             // Person { Name = Alice, Age = 30 }
Compare this to a regular class, where == checks reference identity by default. Use records when you want data containers with structural equality.

S10.2 Positional Records

The compact syntax record T(Type Prop, ...) declares a primary constructor and generates matching init-only properties. You can still add extra members:
record Point(double X, double Y)
{
    // Computed property - not part of equality
    public double Length => Math.Sqrt(X * X + Y * Y);

    // Additional constructor
    public Point(double v) : this(v, v) { }
}

var p = new Point(3, 4);
Console.WriteLine(p.Length);   // 5

// Deconstruct into variables
var (x, y) = p;
Console.WriteLine($"x={x}, y={y}");

S10.3 Non-Destructive Mutation with with

Records are immutable by default. The with expression creates a copy with specified properties changed - the original is unchanged:
record Address(string Street, string City, string Zip);

var home = new Address("123 Main St", "Springfield", "62701");

// Change only the city; everything else is copied
var relocated = home with { City = "Shelbyville", Zip = "62702" };

Console.WriteLine(home);       // Address { Street = 123 Main St, City = Springfield, Zip = 62701 }
Console.WriteLine(relocated);  // Address { Street = 123 Main St, City = Shelbyville, Zip = 62702 }
Console.WriteLine(home == relocated);  // False
with is the idiomatic way to model change in immutable data. It avoids accidental mutation while remaining concise.

S10.4 init-only Properties

The init accessor allows a property to be set during object initialization but not afterward. You can combine this with object initializers on non-positional records:
record Config
{
    public string Host { get; init; } = "localhost";
    public int    Port { get; init; } = 8080;
    public bool   Tls  { get; init; } = false;
}

var cfg = new Config { Host = "prod.example.com", Port = 443, Tls = true };
Console.WriteLine(cfg);

// cfg.Port = 9090;   // compile error - init-only after construction

// Non-positional records still support with
var dev = cfg with { Host = "dev.example.com", Tls = false };

S10.5 Record Structs

record struct gives value-type semantics (stack allocation, copy on assignment) while keeping the auto-generated equality and ToString:
record struct Vector2(double X, double Y)
{
    public static Vector2 operator +(Vector2 a, Vector2 b) => new(a.X + b.X, a.Y + b.Y);
    public double Length => Math.Sqrt(X * X + Y * Y);
}

var v1 = new Vector2(1, 2);
var v2 = new Vector2(3, 4);
Console.WriteLine(v1 + v2);      // Vector2 { X = 4, Y = 6 }
Console.WriteLine(v1 == v2);     // False

// readonly record struct - all properties immutable
readonly record struct Color(byte R, byte G, byte B);
Prefer record struct for small data (2-4 fields) that is passed by value and compared by value - coordinates, colors, keys.

S10.6 Inheritance and Pattern Matching

Records support inheritance. Combined with pattern matching, they model discriminated-union-like hierarchies cleanly:
abstract record Shape;
record Circle(double Radius) : Shape;
record Rectangle(double Width, double Height) : Shape;
record Triangle(double Base, double Height) : Shape;

static double Area(Shape s) => s switch
{
    Circle c     => Math.PI * c.Radius * c.Radius,
    Rectangle r  => r.Width * r.Height,
    Triangle t   => 0.5 * t.Base * t.Height,
    _            => throw new ArgumentException("unknown shape")
};

Console.WriteLine(Area(new Circle(5)));          // 78.54
Console.WriteLine(Area(new Rectangle(4, 6)));    // 24
Console.WriteLine(Area(new Triangle(3, 8)));     // 12

S10.7 Example - All Together

// Records - immutable order pipeline with with-expressions.

record Product(string Name, decimal Price);
record OrderLine(Product Item, int Qty)
{
    public decimal Total => Item.Price * Qty;
}
record Order(string Customer, IReadOnlyList<OrderLine> Lines)
{
    public decimal GrandTotal => Lines.Sum(l => l.Total);
}

var widget  = new Product("Widget", 9.99m);
var gadget  = new Product("Gadget", 49.95m);

var order = new Order("Alice", new[]
{
    new OrderLine(widget, 3),
    new OrderLine(gadget, 1),
});

Console.WriteLine($"Customer: {order.Customer}");
foreach (var line in order.Lines)
    Console.WriteLine($"  {line.Item.Name} x{line.Qty} = {line.Total:C}");
Console.WriteLine($"Total: {order.GrandTotal:C}");

// Update price without mutating the original
var discountedWidget = widget with { Price = 7.99m };
Console.WriteLine($"Sale price: {discountedWidget.Price:C}");

S10.8 Exercise

Exercise
  • Define a record Temperature(double Value, string Unit). Add a method ToCelsius() that converts Fahrenheit and Kelvin to Celsius. Use with to produce a Celsius version of a given temperature.
  • Model a simple event system: define an abstract record Event with derived records UserCreated, UserDeleted, and EmailChanged. Write a Describe(Event e) method using a switch expression.
  • Use readonly record struct to model an RGB color. Add a method Blend(Color other) that averages the two colors. Verify value equality works correctly.

S10.9 Common Mistakes

Mutable properties break the value equality contract

record BadPoint(double X, double Y)
{
    public double Z { get; set; }   // mutable - excluded from equality!
}

var a = new BadPoint(1, 2) { Z = 3 };
var b = new BadPoint(1, 2) { Z = 9 };
Console.WriteLine(a == b);   // True - Z is not in equality check
Only positional parameters and init-only properties are included in auto-generated equality. Mutable set properties are silently excluded.

Confusing record class and record struct

record (or record class) is a reference type - two variables can point to the same object. record struct is a value type - assigning copies the data. Choose based on size and usage pattern, not just on needing value equality.

S10.10 Key Terms

TermMeaning
recordReference type with compiler-generated value equality, ToString, and deconstruct
positional recordrecord T(Type Prop, ...) shorthand generating primary constructor and init properties
with expressionCreates a copy of a record with specified properties changed; original unchanged
init accessorProperty setter that can only be called during object initialization
record structValue-type record; stack-allocated, copied on assignment
readonly record structrecord struct where all properties are immutable after construction
value equalityEquality based on property values, not object identity
deconstructPositional records generate Deconstruct() enabling var (x, y) = point syntax