Modern C# Language Features: Pattern Matching, Records, and More

2026/05/0510 min read
bookmark this

Table of Contents

  1. Pattern Matching (C# 7–13)
  2. Records (C# 9.0 / 10.0)
  3. required Members (C# 11)
  4. Collection Expressions (C# 12)
  5. Primary Constructors for Classes (C# 12)
  6. file-Scoped Types (C# 11)
  7. global using & Implicit Usings (C# 10)
  8. Raw String Literals (C# 11)
  9. params Collections (C# 13)
  10. Other Notable Features

Pattern Matching (C# 7–13)

Pattern matching lets you test a value against a shape, type, or condition — and extract data in the process. It has evolved significantly across C# versions, from simple type checks to complex deconstruction.

Type Patterns (C# 7.0)

// Test and cast in one step
object obj = GetValue();

if (obj is string text)
    Console.WriteLine(text.Length); // text is already a string

if (obj is int number and number > 0)
    Console.WriteLine($"Positive: {number}");

// Switch with type patterns
string Describe(object value) => value switch
{
    int i => $"Integer: {i}",
    string s => $"String: {s}",
    null => "null",
    _ => $"Unknown: {value.GetType().Name}"
};

Property Patterns (C# 8.0)

// Match on properties of an object
string GetShippingCost(Order order) => order switch
{
    { Total: > 100, IsPremium: true } => "Free",
    { Total: > 100 } => "$5.00",
    { Weight: > 50 } => "$25.00",
    _ => "$10.00"
};

// Nested property patterns
bool IsLocalCustomer(Order order) => order is
{
    Customer.Address.Country: "US",
    Customer.Address.State: "CA"
};

Positional Patterns (C# 8.0)

// Works with types that have Deconstruct or are records/tuples
public record Point(int X, int Y);

string Classify(Point point) => point switch
{
    (0, 0) => "Origin",
    (var x, 0) => $"On X-axis at {x}",
    (0, var y) => $"On Y-axis at {y}",
    (var x, var y) when x == y => $"On diagonal at ({x}, {y})",
    _ => "Elsewhere"
};

Relational & Logical Patterns (C# 9.0)

// Relational: <, >, <=, >=
string GetTemperatureCategory(double temp) => temp switch
{
    < 0 => "Freezing",
    >= 0 and < 15 => "Cold",
    >= 15 and < 25 => "Pleasant",
    >= 25 and < 35 => "Warm",
    >= 35 => "Hot"
};

// Logical: and, or, not
bool IsValidAge(int age) => age is >= 0 and <= 150;
bool IsNotNull(object? obj) => obj is not null;
bool IsWeekend(DayOfWeek day) => day is DayOfWeek.Saturday or DayOfWeek.Sunday;

// Combined
string Classify(int value) => value switch
{
    > 0 and < 10 => "Small positive",
    >= 10 and < 100 => "Medium positive",
    >= 100 => "Large positive",
    0 => "Zero",
    < 0 => "Negative"
};

List Patterns (C# 11)

// Match against array/list structure
int[] numbers = [1, 2, 3, 4, 5];

var result = numbers switch
{
    [] => "Empty",
    [var single] => $"Single: {single}",
    [var first, .., var last] => $"First: {first}, Last: {last}",
    [1, 2, ..] => "Starts with 1, 2",
    [.., 4, 5] => "Ends with 4, 5",
};

// Slice pattern with discard
bool IsValid(int[] data) => data is [> 0, .., > 0]; // first and last are positive

// With ReadOnlySpan<char> for string parsing
bool IsCsvHeader(ReadOnlySpan<char> line) => line switch
{
    ['#', ..] => false,        // comment line
    ['"', .., '"'] => true,    // quoted header
    _ => true
};

Records (C# 9.0 / 10.0)

Records are reference types (or value types with record struct) that provide value-based equality, immutability, and deconstruction with minimal boilerplate.

// Record class (reference type, C# 9.0)
public record OrderDto(int Id, string CustomerName, decimal Total);

// Record struct (value type, C# 10.0)
public readonly record struct Money(decimal Amount, string Currency);

// What you get for free:
// • Value-based Equals and GetHashCode
// • ToString (e.g., "OrderDto { Id = 1, CustomerName = Alice, Total = 99.99 }")
// • Deconstruction
// • with-expression for non-destructive mutation

var order = new OrderDto(1, "Alice", 99.99m);
var updated = order with { Total = 150m }; // new instance, only Total changed

var (id, name, total) = order; // deconstruction

Record vs Class vs Struct

Feature record record struct class struct
Equality Value-based Value-based Reference-based Value-based (slow)
Heap/Stack Heap Stack Heap Stack
with expression
Inheritance
ToString Auto-generated Auto-generated GetType().Name GetType().Name
Best for DTOs, events, value objects Small value objects Complex entities Low-level perf

required Members (C# 11)

Forces callers to set specific properties at construction time — compile-time enforcement.

public class CreateOrderRequest
{
    public required string CustomerId { get; init; }
    public required List<OrderItemRequest> Items { get; init; }
    public string? Notes { get; init; }  // optional
}

// ✓ Must set required properties
var request = new CreateOrderRequest
{
    CustomerId = "cust-1",
    Items = [new("SKU-1", 2)]
};

// ✗ Compile error — CustomerId is required
var bad = new CreateOrderRequest
{
    Items = [new("SKU-1", 2)]
}; // CS9035: Required member 'CustomerId' must be set

Use required instead of constructor parameters when you want object-initializer syntax with compile-time safety.


Collection Expressions (C# 12)

A concise syntax for creating collections — works with arrays, List<T>, Span<T>, ImmutableArray<T>, and more.

// Before
int[] numbers = new int[] { 1, 2, 3 };
List<string> names = new List<string> { "Alice", "Bob" };
Span<int> span = stackalloc int[] { 1, 2, 3 };

// After (C# 12)
int[] numbers = [1, 2, 3];
List<string> names = ["Alice", "Bob"];
Span<int> span = [1, 2, 3];

// Empty collections
List<Order> orders = [];
int[] empty = [];

// Spread operator (..) — combine collections
int[] first = [1, 2, 3];
int[] second = [4, 5, 6];
int[] combined = [..first, ..second]; // [1, 2, 3, 4, 5, 6]

// Works with any target type that supports collection expressions
ImmutableArray<int> immutable = [1, 2, 3];
HashSet<string> set = ["a", "b", "c"];

Primary Constructors for Classes (C# 12)

Classes can now have primary constructors — parameters are captured and available throughout the class.

// Before
public class OrderService
{
    private readonly IOrderRepository _repo;
    private readonly ILogger<OrderService> _logger;

    public OrderService(IOrderRepository repo, ILogger<OrderService> logger)
    {
        _repo = repo;
        _logger = logger;
    }
}

// After (C# 12)
public class OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
    public async Task<Order?> GetByIdAsync(int id, CancellationToken ct)
    {
        logger.LogInformation("Fetching order {OrderId}", id);
        return await repo.GetByIdAsync(id, ct);
    }
}

Note: Primary constructor parameters are not readonly by default — see your clean-code guidelines for the full details.


file-Scoped Types (C# 11)

A type visible only within the file where it's declared — perfect for hiding implementation details.

// OrderService.cs

public class OrderService(IOrderRepository repo)
{
    public async Task<OrderResult> ProcessAsync(Order order, CancellationToken ct)
    {
        var validator = new OrderValidator();
        if (!validator.IsValid(order))
            return OrderResult.Invalid(validator.Errors);

        await repo.SaveAsync(order, ct);
        return OrderResult.Success(order);
    }
}

// Only visible in this file — won't pollute the namespace
file class OrderValidator
{
    public List<string> Errors { get; } = [];

    public bool IsValid(Order order)
    {
        if (order.Total <= 0)
            Errors.Add("Total must be positive");
        if (string.IsNullOrWhiteSpace(order.CustomerName))
            Errors.Add("Customer name is required");
        return Errors.Count == 0;
    }
}

global using & Implicit Usings (C# 10)

// GlobalUsings.cs — declare once, available everywhere in the project
global using System.Collections.Concurrent;
global using Microsoft.Extensions.Logging;
global using MyApp.Domain.Entities;
global using MyApp.Application.Interfaces;

// Implicit usings (.NET 6+ — enabled by default in .csproj)
// <ImplicitUsings>enable</ImplicitUsings>
// Automatically adds: System, System.Collections.Generic, System.Linq,
// System.Threading.Tasks, System.IO, etc.

Raw String Literals (C# 11)

// Multi-line strings without escape characters
string json = """
    {
        "name": "Alice",
        "age": 30,
        "orders": [
            { "id": 1, "total": 99.99 }
        ]
    }
    """;

// Interpolated raw strings — use $$ with {{ }}
string name = "Alice";
string jsonTemplate = $$"""
    {
        "name": "{{name}}",
        "timestamp": "{{DateTime.UtcNow:O}}"
    }
    """;

params Collections (C# 13)

params now works with any collection type, not just arrays.

// Before (C# 1.0 — arrays only)
public void Log(params string[] messages) { }

// After (C# 13 — any collection)
public void Log(params ReadOnlySpan<string> messages)
{
    foreach (var msg in messages)
        Console.WriteLine(msg);
}

public void Process(params IEnumerable<int> values) { }
public void Build(params List<string> items) { }

// Still called the same way
Log("hello", "world");
Process(1, 2, 3);

💡 params ReadOnlySpan<T> avoids the array allocation of params T[].


Other Notable Features

using Declarations (C# 8.0)

No braces needed — automatically disposed at end of scope.

// Before
using (var stream = File.OpenRead("data.txt"))
{
    // use stream
}

// After — disposed when the enclosing block (method, if, etc.) exits
using var stream = File.OpenRead("data.txt");
// use stream

Switch Expressions (C# 8.0)

string result = status switch
{
    OrderStatus.Pending => "Processing",
    OrderStatus.Shipped => "On the way",
    OrderStatus.Delivered => "Complete",
    _ => "Unknown"
};

Null-Coalescing Assignment ??= (C# 8.0)

_cache ??= new Dictionary<string, int>();

Target-Typed new (C# 9.0)

Dictionary<string, List<int>> map = new(); // type inferred from left side

File-Scoped Namespaces (C# 10)

// Before
namespace MyApp.Services
{
    public class OrderService { }
}

// After — saves one level of indentation
namespace MyApp.Services;

public class OrderService { }

Generic Attributes (C# 11)

// Before — typeof required
[Validator(typeof(OrderValidator))]
public class Order { }

// After — generic attribute
[Validator<OrderValidator>]
public class Order { }

nameof Scope Improvement (C# 11)

// nameof works on method parameters in attributes
[return: NotNullIfNotNull(nameof(input))]
public static string? Process(string? input) => input?.Trim();

Feature Version Quick Reference

Feature C# Version .NET Version
Pattern matching (basic) 7.0 .NET Core 2.0
Span<T>, ref struct 7.2 .NET Core 2.1
Nullable reference types 8.0 .NET Core 3.0
using declarations 8.0 .NET Core 3.0
Switch expressions 8.0 .NET Core 3.0
Default interface methods 8.0 .NET Core 3.0
IAsyncEnumerable<T> 8.0 .NET Core 3.0
Records 9.0 .NET 5
Relational / logical patterns 9.0 .NET 5
Target-typed new 9.0 .NET 5
global using 10.0 .NET 6
File-scoped namespaces 10.0 .NET 6
Record structs 10.0 .NET 6
Natural lambda types 10.0 .NET 6
Raw string literals 11.0 .NET 7
List patterns 11.0 .NET 7
required members 11.0 .NET 7
file-scoped types 11.0 .NET 7
Generic attributes 11.0 .NET 7
Static abstract interface members 11.0 .NET 7
Generic math (INumber<T>) 11.0 .NET 7
Primary constructors (classes) 12.0 .NET 8
Collection expressions [1,2,3] 12.0 .NET 8
params collections 13.0 .NET 9
Lock type 13.0 .NET 9
Task.WhenEach 13.0 .NET 9
Extension members (extension blocks) 14.0 .NET 10
field keyword in properties 14.0 .NET 10
Null-conditional assignment ?.= 14.0 .NET 10
Unbound generics in nameof 14.0 .NET 10

Summary — Modern C# Checklist

Feature Use For
Pattern matching Replace if/switch chains with expressive matching
Records DTOs, events, value objects — value equality + immutability
required Enforce property initialization at compile time
Collection expressions [1, 2, 3] — concise collection creation
Primary constructors DI injection in services — less boilerplate
file types Hide implementation details within a file
global using Reduce repetitive using directives
Raw strings JSON, SQL, multi-line text without escaping
params spans Variable arguments without array allocation
List patterns Destructure arrays/lists in switch expressions