Engineering Notes
Home/Languages/C#/Modern C#: Records, Pattern Matching and Nullable Reference Types
Languages · C#

Modern C#: Records, Pattern Matching and Nullable Reference Types

Write idiomatic modern C#: records and value equality, with-expressions, pattern matching from property to list patterns, nullable reference types, required members, primary constructors and collection expressions.

💻 Languages● Intermediate⏱ 3 min read🛠️ 2 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • Records, record structs and with-expressions
  • Switch expressions and every kind of pattern
  • Nullable reference types in practice
  • Required members, primary constructors and collection expressions

📚 Before you start

1Records

Records are types with value-based equality, built-in ToString, deconstruction and non-destructive mutation with with:

csharp
public record Money(decimal Amount, string Currency);           // reference type, immutable positional properties
public readonly record struct Point(int X, int Y);              // value type
var price = new Money(10m, "EUR");
var discounted = price with { Amount = 8m };                    // copy with one change
Console.WriteLine(price == new Money(10m, "EUR"));              // True: compares values

Use records for DTOs, events, value objects and messages. Classes remain right for entities with identity and behaviour.

2Pattern Matching

csharp
string Describe(Shape s) => s switch
{
    Circle { Radius: 0 }            => "point",
    Circle c                        => $"circle r={c.Radius}",
    Rectangle { Width: var w, Height: var h } when w == h => $"square {w}",
    Rectangle r                     => $"rect {r.Width}x{r.Height}",
    null                            => "nothing",
    _                               => "unknown shape",
};

string Grade(int score) => score switch { >= 90 => "A", >= 80 and < 90 => "B", < 0 or > 100 => "invalid", _ => "C" };

string Summarize(int[] xs) => xs switch { [] => "empty", [var only] => $"one: {only}", [var first, .., var last] => $"{first}..{last}" };

Type patterns, property patterns, relational (>=), logical (and, or, not), positional patterns (deconstruction) and list patterns (C# 11) make branching declarative, and the compiler warns when a switch expression isn't exhaustive.

3Nullable Reference Types

With <Nullable>enable</Nullable>, string means "never null" and string? means "may be null". The compiler tracks null-state through your code and warns on possible null dereferences or on assigning null to non-nullable types.

  • Check with is null / is not null, use ?. and ??, ??=.
  • Attributes like [NotNullWhen(true)] teach the compiler about your helper methods (e.g. TryGet).
  • The null-forgiving operator ! silences a warning: use rarely, as a statement that you know better.
  • Treat nullable warnings as errors in new code (<WarningsAsErrors>nullable</WarningsAsErrors>).

4Other Features Worth Knowing

  • required members and init setters: objects must be fully initialized at creation, yet immutable afterwards.
  • Primary constructors on classes (C# 12): class OrderService(IOrderRepository repo, ILogger<OrderService> log).
  • Collection expressions (C# 12): int[] xs = [1, 2, 3];, List<string> names = [..first, ..second];.
  • File-scoped namespaces, global usings, raw string literals ("""…"""), switch on tuples.
  • Static abstract interface members enable generic math (INumber<T>).

⚠️ Common Pitfalls

  1. Records with mutable collections: equality compares the collection reference, not contents.
  2. Using records for EF entities with value equality: identity semantics get confusing.
  3. Non-exhaustive switch expressions that throw SwitchExpressionException at runtime.
  4. Sprinkling ! to silence nullable warnings instead of fixing the flow.
  5. Overusing clever patterns where a simple if reads better.

📋 Quick Reference

text
record = value equality + with + ToString · record struct for small values · classes for entities
switch expressions: type, property, relational, logical, positional, list patterns · exhaustive
Nullable enabled: string vs string? · is null · ?. ?? ??= · [NotNullWhen] · avoid !
required/init · primary constructors · collection expressions [..a, ..b] · raw strings

🧠 Key takeaways

  • Records give value equality and with; use them for DTOs, messages and value objects.
  • Switch expressions with property, relational, logical and list patterns replace if-chains.
  • Enable nullable reference types and fix warnings instead of sprinkling !.
  • Use required/init, primary constructors and collection expressions for concise, safe code.

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1When would you use a record instead of a class?core▾

For immutable data defined by its values: DTOs, API contracts, events/messages, value objects. Records give value equality, with-expressions, deconstruction and ToString. Use classes for entities with identity and mutable behaviour.

Follow-ups interviewers ask
  • record class vs record struct? Reference vs value type; record struct for small values.
Q2What kinds of patterns does C# support?core▾

Type, constant, property, positional (deconstruction), relational (>), logical (and/or/not), var, discard, and list patterns ([first, .., last]), usable in is, switch statements and switch expressions with when guards.

Follow-ups interviewers ask
  • Exhaustiveness? The compiler warns when a switch expression doesn't cover all inputs.
Q3How do nullable reference types work?core▾

A compile-time feature: with nullable enabled, reference types are non-null by default and T? marks possibly-null ones; flow analysis warns on possible null dereference. Nothing changes at runtime, so validate external input.

Follow-ups interviewers ask
  • [NotNullWhen(true)]? Tells the compiler an out value isn't null when the method returns true.
Q4What problems do required and init solve?senior▾

init allows setting properties only during object initialization (immutable afterwards); required forces callers to set them, so objects can't be created half-initialized without constructors for every combination.

Follow-ups interviewers ask
  • Works with records? Yes.
Q5What are primary constructors on classes?core▾

C# 12 lets classes declare constructor parameters in the type header; parameters are captured and usable throughout the class, reducing DI boilerplate. They aren't automatically readonly properties (unlike positional records).

Follow-ups interviewers ask
  • Gotcha? Parameters are mutable captured variables; assign to readonly fields if needed.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.

✅ Check yourself

Answer the questions — feedback is instant.

1. new Money(10, "EUR") == new Money(10, "EUR") for a record?
Value equality.
2. What does p with { X = 5 } do?
Non-destructive mutation.
3. Which pattern matches arrays starting with 1?
List pattern.
4. With nullable enabled, what does string? mean?
Annotated reference type.