iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Records

C# records are reference types with value-based equality, with-expressions, and concise syntax. record for classes; record struct for value-type variants. Built for DTOs and immutable data.

Records, with, inheritance, struct records

EXAMPLE
using System;
using System.Collections.Generic;

// 1) Positional record — properties + ctor + equality auto-generated
public record Person(string FirstName, string LastName, int Age);

var p = new Person("Ada", "Lovelace", 32);
Console.WriteLine(p);                       // Person { FirstName = Ada, LastName = Lovelace, Age = 32 }
Console.WriteLine(p.FirstName);

// 2) Value-based equality (NOT reference)
var p2 = new Person("Ada", "Lovelace", 32);
Console.WriteLine(p == p2);                 // true
Console.WriteLine(ReferenceEquals(p, p2));  // false
Console.WriteLine(p.GetHashCode() == p2.GetHashCode());  // true

// 3) with-expression — non-destructive mutation
var older = p with { Age = 33 };            // new Person, original unchanged
var married = p with { LastName = "King" };

// 4) Init-only props (default for record positional)
public record Book
{
    public required string Title { get; init; }
    public required string Author { get; init; }
    public int Pages { get; init; } = 0;
}

var b = new Book { Title = "Hi", Author = "x", Pages = 200 };
// b.Title = "new";  // ERROR — init-only after construction

// 5) Record inheritance
public record Shape(double Area);
public record Circle(double Radius) : Shape(Math.PI * Radius * Radius);
public record Rect(double Width, double Height) : Shape(Width * Height);

Shape s = new Circle(5);
if (s is Circle c) Console.WriteLine(c.Radius);

// 6) Deconstruction
var (first, last, age) = p;

// 7) record struct (C# 10+) — value-type record
public readonly record struct Point(double X, double Y);

var a = new Point(1, 2);
var c2 = a with { X = 10 };                  // new struct, original unchanged
Console.WriteLine(a == new Point(1, 2));     // true — value semantics

// 8) Equality customisation
public record User(string Email)
{
    public string Email { get; init; } = Email.ToLowerInvariant();

    public virtual bool Equals(User other) =>
        other is not null && Email.Equals(other.Email, StringComparison.OrdinalIgnoreCase);

    public override int GetHashCode() => Email.ToLowerInvariant().GetHashCode();
}

// 9) Records in patterns (super clean)
static string Describe(Shape s) => s switch
{
    Circle { Radius: > 10 } big       => $"big circle r={big.Radius}",
    Circle c                          => $"small circle r={c.Radius}",
    Rect   { Width: var w, Height: var h } when w == h => $"square {w}",
    Rect   r                          => $"rect {r.Width}x{r.Height}",
    _ => "unknown",
};

// 10) Computed properties
public record Money(decimal Amount, string Currency)
{
    public string Display => $"{Amount:F2} {Currency}";
}

var m = new Money(9.99m, "AUD");
Console.WriteLine(m.Display);

// 11) JSON serialisation (System.Text.Json) — Just Works
using System.Text.Json;
var json = JsonSerializer.Serialize(p);
var back = JsonSerializer.Deserialize<Person>(json);
Console.WriteLine(p == back);                // true

// 12) Records as discriminated-union members
abstract record Event;
record UserCreated(Guid Id, string Email) : Event;
record UserDeleted(Guid Id, string Reason) : Event;
record UserVerified(Guid Id, DateTime At)  : Event;

static string Handle(Event e) => e switch
{
    UserCreated u  => $"created {u.Id} {u.Email}",
    UserDeleted u  => $"deleted {u.Id}: {u.Reason}",
    UserVerified u => $"verified {u.Id} at {u.At}",
    _              => throw new ArgumentException(),
};

// 13) Records vs class vs struct — when to use which
// record class  : DTO, value-equality semantics, reference type. Most common record.
// record struct : tiny values you copy (Point, Money, Color), value-equality, value type.
// class         : identity-based, mutable, longer lifetime, behavior with state.
// struct        : tiny, no behavior, manual equality if needed.

// 14) Custom ctor + extra members
public record Order(string Id, decimal Total)
{
    public DateTime CreatedAt { get; init; } = DateTime.UtcNow;

    public Order(string id) : this(id, 0m) { }

    public Order Apply(decimal extra) => this with { Total = Total + extra };
}

// 15) Sealed records — block further inheritance
public sealed record FinalNote(string Text);

// 16) Pitfalls
//   • Mutable members inside a record — reference equality of nested mutable types breaks Equals
//   • Records with collections — collection identity matters; consider ImmutableList
//   • Adding behavior that doesn't fit the data-class shape — use a class instead
//   • Init-only doesn't prevent mutation of nested mutable objects

Why it matters

Records replace 80% of POCOs / DTOs. Value-based equality + with expressions + pattern matching = compact, safe domain models. Reach for records first; promote to class only when identity matters.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
public record Point(int X, int Y);
var p = new Point(3, 4);
var p2 = p with { X = 5 };       // non-destructive copy
Try it Yourself »

Test yourself

Q1. A record gets value-based equality…
Q2. Non-destructive copy uses…
Q3. Records are best used for…

Discussion

Loading…