C# Records & Pattern Matching Cheat Sheet
Explains C# records for immutable value-based types, the with expression, and modern pattern matching including property and positional patterns.
Records
Immutable, value-equal reference and value types.
public record Point(int X, int Y); // Positional record, immutable by defaultvar p1 = new Point(1, 2);var p2 = new Point(1, 2);Console.WriteLine(p1 == p2); // True: value-based equalityvar p3 = p1 with { Y = 5 }; // Non-destructive mutation: copies p1, changes YConsole.WriteLine(p3); // Point { X = 1, Y = 5 } (auto ToString)public record class Person(string Name, int Age); // record class (reference type, the default)public record struct Coord(double Lat, double Lng); // record struct (value type)
Pattern Matching
Switch expressions with type, relational, and logical patterns.
object value = 42;string description = value switch{ int n when n < 0 => "negative", 0 => "zero", int n and > 0 and < 10 => "small positive", // Relational + logical patterns int => "large integer", string s => $"a string: {s}", null => "nothing", _ => "unknown"};if (value is int number && number > 10) // Type pattern + condition{ Console.WriteLine($"Big number: {number}");}
Deconstruction & Record Patterns
Pulling positional and property values out of a record.
var point = new Point(3, 4);var (x, y) = point; // Deconstruction (auto-generated for records)string quadrant = point switch{ Point { X: > 0, Y: > 0 } => "Q1", // Property pattern matching on record members Point { X: < 0, Y: > 0 } => "Q2", Point(0, 0) => "Origin", // Positional record pattern _ => "Other"};
Key Concepts
What makes records and pattern matching distinct.
- Value equality- Records override Equals/GetHashCode to compare by value, not by reference
- with expression- Creates a shallow copy with specified properties changed (non-destructive mutation)
- record struct- A C# 10+ value-type record; avoids heap allocation for small immutable data
- init accessors- public int X { get; init; } allows setting a property only during object initialization
- Switch expressions- x switch { ... } is an expression form of switch that must be exhaustive or have a _ arm
- Pattern kinds- constant, type, relational (>, <), logical (and/or/not), property, positional, and list patterns
List Patterns (C# 11)
Matching the shape and contents of arrays/spans without manual indexing.
int[] numbers = { 1, 2, 3, 4, 5 };string result = numbers switch{ [] => "empty", [var single] => $"one element: {single}", [var first, .., var last] => $"starts {first}, ends {last}", [1, 2, ..] => "starts with 1, 2", _ => "other"};if (numbers is [var head, .. var rest]) // Slice pattern captures the remainder{ Console.WriteLine($"head={head}, rest.Length={rest.Length}");}
Record Inheritance & Sealed Hierarchies
Modeling discriminated unions with records for exhaustive pattern matching.
public abstract record Shape;public sealed record Circle(double Radius) : Shape;public sealed record Rectangle(double Width, double Height) : Shape;public sealed record Triangle(double Base, double Height) : Shape;public static double Area(Shape shape) => shape 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 NotSupportedException() // Needed since Shape isn't a closed union};// Derived records automatically extend base positional parameters and inherit value equality,// but two records are only equal if they're the exact same runtime type.
Overriding Generated Equality
Customizing how a record compares itself when the default member-wise comparison isn't enough.
public record CaseInsensitiveName(string Value){ public virtual bool Equals(CaseInsensitiveName? other) => other is not null && string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); public override int GetHashCode() => StringComparer.OrdinalIgnoreCase.GetHashCode(Value);}var a = new CaseInsensitiveName("Alice");var b = new CaseInsensitiveName("ALICE");Console.WriteLine(a == b); // True, thanks to the custom Equals override// Note: the compiler still generates a sealed ToString() override with 'protected' PrintMembers// that you can override too by declaring your own 'protected override bool PrintMembers(...)'.
Nested & Extended Property Patterns
Matching deep into an object graph in a single expression.
public record Address(string City, string Country);public record Customer(string Name, Address Address, int OrderCount);string Classify(Customer c) => c switch{ { Address.Country: "US", OrderCount: > 100 } => "VIP US customer", // Extended (dotted) property pattern { Address: { City: "London", Country: "UK" } } => "UK - London", // Nested property pattern { OrderCount: 0 } => "prospect", not { Address: null } => "has address on file", // 'not' pattern _ => "other"};
readonly record struct & Primary Constructors
Squeezing out allocations and using C# 12 primary constructors on plain classes.
public readonly record struct Money(decimal Amount, string Currency){ public static Money operator +(Money a, Money b) => a.Currency == b.Currency ? a with { Amount = a.Amount + b.Amount } : throw new InvalidOperationException("Currency mismatch");}// C# 12 primary constructors: any class, not just records, can take constructor params directlypublic class OrderProcessor(IOrderRepository repo, ILogger<OrderProcessor> logger){ public async Task ProcessAsync(int id) { logger.LogInformation("Processing {Id}", id); await repo.MarkProcessedAsync(id); }}
Advanced Pattern-Matching Vocabulary
Terms that show up once you go past basic switch expressions.
- var pattern- { X: var x } binds a name to a value unconditionally, useful for capturing inside a nested pattern
- Slice pattern (..)- Matches zero or more elements in a list pattern; can optionally capture the slice into a variable
- Discard pattern (_)- Matches anything without binding it; also used as the mandatory default arm in exhaustive switches
- is not null- Idiomatic C# 9+ null check via negated pattern instead of != null
- PrintMembers- Compiler-generated hook records use to build their ToString(); override it to customize output
- EqualityContract- Protected virtual property the compiler emits so equality also considers the runtime type in a hierarchy
- Positional deconstruction depth- Record patterns can nest positional and property patterns arbitrarily: Point(> 0, > 0) { }
Use record struct for small, frequently created immutable value types like coordinates or money amounts to get value semantics without the heap allocation and GC pressure of a record class.