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 »
Discussion
Loading…