{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreifzkaywdzk4s7i4rxjunpva5tpq75xfzcspg6lftx7hnidq3qqwoq",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpvd3dm56g72"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreibf7stx3rckjdk4pduhtyjgfmedukzmetsnrkmkvlggcesuhg3edq"
    },
    "mimeType": "image/webp",
    "size": 383764
  },
  "path": "/rhuturaj_takle/modern-c-features-a-deep-dive-into-records-pattern-matching-async-and-performance-3a6",
  "publishedAt": "2026-07-05T09:40:06.000Z",
  "site": "https://dev.to",
  "tags": [
    "csharp",
    "programming",
    "webdev",
    "learning"
  ],
  "textContent": "#  Modern C# Features: A Deep Dive into Records, Pattern Matching, Async, and Performance\n\n_A practical guide to the C# language features that have reshaped how we write .NET code — records, pattern matching, async/await improvements, nullable reference types, LINQ enhancements,`Span<T>`, and performance optimizations._\n\n##  Table of Contents\n\n  1. Introduction\n  2. Records\n  3. Pattern Matching\n  4. Async/Await Improvements\n  5. Nullable Reference Types\n  6. LINQ Enhancements\n  7. Span<T> and Memory<T>\n  8. Performance Optimizations\n  9. Quick Reference Table\n  10. Conclusion\n\n\n\n##  Introduction\n\nC# has evolved significantly since C# 8. Each release (9, 10, 11, 12, 13) has focused on three consistent themes:\n\n  * **Conciseness** — write less boilerplate to express the same intent.\n  * **Safety** — catch bugs at compile time instead of runtime (especially around `null`).\n  * **Performance** — give developers low-level control without leaving the managed, safe world of .NET.\n\n\n\nThis guide walks through the features that matter most in day-to-day development, with working code examples you can drop into a `dotnet run` project.\n\n##  1. Records\n\nIntroduced in **C# 9** , `record` types give you immutable, value-based data models with almost no ceremony.\n\n###  Why records exist\n\nBefore records, representing an immutable data object meant hand-writing a constructor, `Equals`, `GetHashCode`, `ToString`, and often a `With`-style copy method. Records generate all of this for you.\n\n\n\n    // Before: a \"plain\" immutable class\n    public class PersonClass\n    {\n        public string FirstName { get; }\n        public string LastName { get; }\n\n        public PersonClass(string firstName, string lastName)\n        {\n            FirstName = firstName;\n            LastName = lastName;\n        }\n\n        public override bool Equals(object? obj) =>\n            obj is PersonClass p && p.FirstName == FirstName && p.LastName == LastName;\n\n        public override int GetHashCode() => HashCode.Combine(FirstName, LastName);\n\n        public override string ToString() => $\"PersonClass {{ FirstName = {FirstName}, LastName = {LastName} }}\";\n    }\n\n    // After: the same thing as a record\n    public record Person(string FirstName, string LastName);\n\n\nThat one line gives you:\n\n  * Value-based equality (`Equals`/`GetHashCode`)\n  * A readable `ToString()` override\n  * A deconstructor (`var (first, last) = person;`)\n  * Immutability by default (`init`-only properties)\n\n\n\n###  Non-destructive mutation with `with`\n\n\n    var person = new Person(\"Ada\", \"Lovelace\");\n    var married = person with { LastName = \"King\" };\n\n    Console.WriteLine(person);  // Person { FirstName = Ada, LastName = Lovelace }\n    Console.WriteLine(married); // Person { FirstName = Ada, LastName = King }\n\n\n`with` copies the object and lets you override specific properties — the rest are copied as-is. This is the idiomatic way to \"mutate\" an immutable object.\n\n###  Record structs (C# 10)\n\nIf you want value-type semantics (stack allocation, no heap overhead) with record-style equality:\n\n\n\n    public readonly record struct Point(double X, double Y);\n\n    var a = new Point(1.0, 2.0);\n    var b = new Point(1.0, 2.0);\n    Console.WriteLine(a == b); // true — structural equality, no boxing\n\n\n###  Class vs. struct records\n\n|  `record class` (default) | `record struct`\n---|---|---\nStorage | Heap | Stack (or inline)\nDefault mutability | Immutable (`init`) | Mutable unless `readonly`\nBest for | Domain models, DTOs | Small, frequently-copied values\n\n###  When to reach for a record\n\n  * DTOs and API contracts\n  * Domain value objects (money, coordinates, ranges)\n  * Anything where \"two objects with the same data are the same object\" is the correct semantics\n\n\n\n##  2. Pattern Matching\n\nPattern matching has grown from a niche `is` operator trick into a full expression language for shape-checking data.\n\n###  Type patterns and property patterns\n\n\n    public static decimal CalculateShipping(object order) => order switch\n    {\n        Order { Total: > 100, IsPriority: true } => 0m,\n        Order { Total: > 100 }                   => 5.99m,\n        Order { IsPriority: true }                => 12.99m,\n        Order o                                    => 9.99m,\n        _                                           => throw new ArgumentException(\"Not an order\")\n    };\n\n\n###  Relational and logical patterns (C# 9)\n\n\n    static string Grade(int score) => score switch\n    {\n        >= 90              => \"A\",\n        >= 80 and < 90     => \"B\",\n        >= 70 and < 80     => \"C\",\n        < 0 or > 100       => throw new ArgumentOutOfRangeException(nameof(score)),\n        _                  => \"F\"\n    };\n\n\n###  List patterns (C# 11)\n\nList patterns let you match on the shape and contents of arrays and lists directly.\n\n\n\n    static string Describe(int[] numbers) => numbers switch\n    {\n        []                    => \"empty\",\n        [var only]            => $\"single element: {only}\",\n        [var first, .., var last] => $\"starts with {first}, ends with {last}\",\n        [1, 2, ..]            => \"starts with 1, 2\",\n        _                     => \"some other sequence\"\n    };\n\n    Describe(Array.Empty<int>());     // \"empty\"\n    Describe(new[] { 42 });           // \"single element: 42\"\n    Describe(new[] { 1, 2, 3, 4 });   // \"starts with 1, 2\"\n\n\n###  Combining patterns with `is` for guard clauses\n\n\n    if (shape is Circle { Radius: > 0 and < 100 } c)\n    {\n        Console.WriteLine($\"Valid circle with radius {c.Radius}\");\n    }\n\n\n###  Why it matters\n\nPattern matching moves validation and branching logic out of nested `if`/`else` pyramids and into declarative, readable expressions — and the compiler checks exhaustiveness on `switch` expressions over closed type hierarchies.\n\n##  3. Async/Await Improvements\n\nAsync/await itself hasn't changed shape, but the surrounding ecosystem has matured a lot.\n\n###  `IAsyncEnumerable<T>` and `await foreach` (C# 8)\n\nStream asynchronous sequences without buffering everything in memory:\n\n\n\n    public static async IAsyncEnumerable<string> ReadLinesAsync(string path)\n    {\n        using var reader = new StreamReader(path);\n        string? line;\n        while ((line = await reader.ReadLineAsync()) is not null)\n        {\n            yield return line;\n        }\n    }\n\n    await foreach (var line in ReadLinesAsync(\"large-file.txt\"))\n    {\n        Console.WriteLine(line);\n    }\n\n\n###  Async streams with cancellation\n\n\n    await foreach (var item in GetItemsAsync().WithCancellation(cancellationToken))\n    {\n        Process(item);\n    }\n\n\n###  `ValueTask<T>` for hot paths\n\nWhen a method often completes synchronously (e.g., cache hits), `ValueTask<T>` avoids allocating a `Task<T>` on every call:\n\n\n\n    public ValueTask<int> GetValueAsync(string key)\n    {\n        if (_cache.TryGetValue(key, out var cached))\n            return new ValueTask<int>(cached); // no allocation\n\n        return new ValueTask<int>(LoadFromDbAsync(key)); // falls back to a real Task\n    }\n\n\n> ⚠️ Rule of thumb: only `await` a `ValueTask` once, and don't call `.Result` or store it for later — its internal representation isn't safe to reuse like `Task`.\n\n###  `Task.WaitAsync` and timeouts (C# 10 / .NET 6+)\n\n\n    try\n    {\n        var result = await SlowOperationAsync().WaitAsync(TimeSpan.FromSeconds(5), cancellationToken);\n    }\n    catch (TimeoutException)\n    {\n        Console.WriteLine(\"Operation timed out.\");\n    }\n\n\n###  `System.Threading.Lock` (C# 13 / .NET 9)\n\nA dedicated lock type replaces the old `lock (object)` pattern with a lighter-weight, non-boxing primitive, and the compiler recognizes it for optimized codegen:\n\n\n\n    private readonly Lock _lock = new();\n\n    public void Increment()\n    {\n        lock (_lock)\n        {\n            _counter++;\n        }\n    }\n\n\n###  Why it matters\n\nThese changes reduce allocations in async-heavy code (a common source of GC pressure in high-throughput services) and make streaming and cancellation first-class citizens instead of afterthoughts.\n\n##  4. Nullable Reference Types\n\nIntroduced in **C# 8** , nullable reference types (NRT) turn `NullReferenceException` from a runtime surprise into a compile-time warning.\n\n###  Enabling it\n\n\n    <!-- in your .csproj -->\n    <Nullable>enable</Nullable>\n\n\nOr per-file:\n\n\n\n    #nullable enable\n\n\n###  Basic usage\n\n\n    public class UserService\n    {\n        public string Name { get; set; } = string.Empty; // non-nullable: must be assigned\n        public string? MiddleName { get; set; }           // nullable: allowed to be null\n\n        public string Greet(string? nickname)\n        {\n            // Compiler warns if you dereference `nickname` without a null check\n            return nickname is not null\n                ? $\"Hey, {nickname}!\"\n                : $\"Hello, {Name}!\";\n        }\n    }\n\n\n###  Null-forgiving operator\n\nSometimes you know better than the compiler (e.g., right after a `TryGetValue`):\n\n\n\n    if (dictionary.TryGetValue(key, out var value))\n    {\n        Use(value!); // tell the compiler: trust me, this isn't null here\n    }\n\n\n###  Attributes that describe null-flow\n\n\n    public bool TryParse(string? input, [NotNullWhen(true)] out Config? config)\n    {\n        if (string.IsNullOrEmpty(input))\n        {\n            config = null;\n            return false;\n        }\n        config = Config.Parse(input);\n        return true;\n    }\n\n\nThe `[NotNullWhen(true)]` attribute tells the compiler that if this method returns `true`, `config` is guaranteed non-null — so callers don't get spurious warnings.\n\n###  Why it matters\n\nNRT doesn't eliminate `NullReferenceException` at runtime (it's a static-analysis feature, not a new type system), but in practice it catches the vast majority of null-handling bugs during code review and CI, long before they reach production.\n\n##  5. LINQ Enhancements\n\nLINQ keeps gaining query operators that used to require third-party libraries or manual loops.\n\n###  `Chunk` (C# 10 / .NET 6)\n\nSplit a sequence into fixed-size batches — great for batched API calls or bulk inserts:\n\n\n\n    int[] numbers = Enumerable.Range(1, 10).ToArray();\n\n    foreach (int[] batch in numbers.Chunk(3))\n    {\n        Console.WriteLine(string.Join(\", \", batch));\n    }\n    // 1, 2, 3\n    // 4, 5, 6\n    // 7, 8, 9\n    // 10\n\n\n###  `MinBy` / `MaxBy` (.NET 6)\n\n\n    var cheapest = products.MinBy(p => p.Price);\n    var mostExpensive = products.MaxBy(p => p.Price);\n\n\nNo more `OrderBy(...).First()` just to find an extremum by a key.\n\n###  `DistinctBy`, `UnionBy`, `IntersectBy`, `ExceptBy` (.NET 6)\n\n\n    var uniqueByEmail = users.DistinctBy(u => u.Email);\n\n\n###  `Zip` with three sequences (.NET 6)\n\n\n    var combined = names.Zip(ages, cities, (name, age, city) => $\"{name} ({age}) from {city}\");\n\n\n###  `Index()` (C# 13 / .NET 9)\n\nGet the index alongside each element without a manual counter:\n\n\n\n    foreach (var (index, value) in items.Index())\n    {\n        Console.WriteLine($\"{index}: {value}\");\n    }\n\n\n###  `AggregateBy` (.NET 9)\n\nGroup-and-aggregate in a single pass, avoiding an intermediate `GroupBy` allocation:\n\n\n\n    var totalsByCategory = orders.AggregateBy(\n        keySelector: o => o.Category,\n        seed: 0m,\n        func: (total, order) => total + order.Amount);\n\n\n###  Why it matters\n\nEach of these operators replaces a common hand-rolled loop or a two-step `OrderBy().First()`/`GroupBy().Select()` pattern with a single, well-tested, often more efficient built-in — less code, fewer bugs, and in several cases (like `MinBy`/`AggregateBy`) genuinely better performance because they avoid full sorts or extra allocations.\n\n##  6. Span<T> and Memory<T>\n\n`Span<T>` (C# 7.2+, but increasingly central in modern C#) is a `ref struct` that represents a contiguous region of memory — array, stack-allocated buffer, or a slice of a string — **without copying it**.\n\n###  Slicing without allocation\n\n\n    string text = \"Hello, World!\";\n    ReadOnlySpan<char> span = text.AsSpan();\n    ReadOnlySpan<char> hello = span.Slice(0, 5); // \"Hello\" — no new string allocated\n\n    Console.WriteLine(hello.ToString());\n\n\nCompare to the traditional approach, `text.Substring(0, 5)`, which allocates a brand-new string every time.\n\n###  Stack allocation with `stackalloc`\n\n\n    Span<int> buffer = stackalloc int[100]; // lives on the stack, no GC involved\n    for (int i = 0; i < buffer.Length; i++)\n    {\n        buffer[i] = i * i;\n    }\n\n\n###  Parsing without allocating substrings\n\n\n    ReadOnlySpan<char> csvLine = \"42,apple,3.99\".AsSpan();\n    int firstComma = csvLine.IndexOf(',');\n    ReadOnlySpan<char> idSpan = csvLine[..firstComma];\n    int id = int.Parse(idSpan); // parses directly from the span, no substring needed\n\n\n###  `Memory<T>` for async scenarios\n\n`Span<T>` is a `ref struct`, so it **cannot** be used across `await` boundaries or stored in fields/heap objects. `Memory<T>` is the heap-friendly counterpart for those cases:\n\n\n\n    public async Task ProcessAsync(Memory<byte> buffer)\n    {\n        await stream.ReadAsync(buffer);\n        Span<byte> span = buffer.Span; // get a Span only when you need synchronous access\n        Process(span);\n    }\n\n\n###  Why it matters\n\n`Span<T>` is one of the biggest reasons modern .NET is fast: string parsing, JSON serialization, and networking code across the BCL (`System.Text.Json`, `Utf8Parser`, socket APIs) are built on spans internally, which is why upgrading the runtime often speeds up code you didn't even touch.\n\n##  7. Performance Optimizations\n\nBeyond specific language features, several changes reduce overhead across the board.\n\n###  Generic math (C# 11)\n\nStatic abstract members in interfaces let you write numeric algorithms once, for any number type, with zero boxing:\n\n\n\n    public static T Sum<T>(IEnumerable<T> values) where T : INumber<T>\n    {\n        T total = T.Zero;\n        foreach (var v in values)\n            total += v;\n        return total;\n    }\n\n    Sum(new[] { 1, 2, 3 });          // works for int\n    Sum(new[] { 1.5, 2.5 });         // and double\n    Sum(new[] { 1m, 2m });           // and decimal — no separate overloads needed\n\n\n###  `required` members (C# 11)\n\nEnforce that a property must be set at construction time — without needing a constructor:\n\n\n\n    public class Config\n    {\n        public required string ConnectionString { get; init; }\n        public int TimeoutSeconds { get; init; } = 30;\n    }\n\n    // Compiler error if ConnectionString is missing:\n    var config = new Config { ConnectionString = \"...\" };\n\n\n###  UTF-8 string literals (C# 11)\n\nSkip the runtime encoding step when you need raw UTF-8 bytes:\n\n\n\n    ReadOnlySpan<byte> utf8 = \"Hello, World!\"u8; // encoded at compile time\n\n\n###  Collection expressions (C# 12)\n\nA single, consistent syntax for constructing arrays, lists, and spans, which the compiler can optimize into the most efficient underlying representation:\n\n\n\n    int[] array = [1, 2, 3];\n    List<int> list = [1, 2, 3];\n    Span<int> span = [1, 2, 3];\n\n    int[] combined = [.. array, 4, 5, 6]; // spread operator\n\n\n###  `params` with `Span<T>` (C# 13)\n\n`params` parameters can now use `Span<T>`/`ReadOnlySpan<T>` instead of always allocating an array:\n\n\n\n    void Log(params ReadOnlySpan<string> messages) { /* ... */ }\n\n\n###  Why it matters\n\nIndividually, these are small wins. Together — generic math avoiding boxing, spans avoiding allocations, collection expressions choosing efficient backing storage, and the JIT's ongoing improvements (tiered PGO, dynamic PGO on by default since .NET 8) — they add up to real, measurable throughput and memory improvements release over release, often without changing a single line of business logic.\n\n##  Quick Reference Table\n\nFeature | Introduced | Problem it Solves\n---|---|---\nRecords | C# 9 | Boilerplate immutable data models\nRecord structs | C# 10 | Value-type records without heap allocation\nPattern matching (relational/logical) | C# 9 | Verbose `if`/`else` chains\nList patterns | C# 11 | Matching array/list shape and contents\n`IAsyncEnumerable<T>` | C# 8 | Streaming async sequences\n`ValueTask<T>` | C# 7+ (widely used now) | Allocation-free sync-complete async paths\n`System.Threading.Lock` | C# 13 | Lighter-weight locking primitive\nNullable reference types | C# 8 | Compile-time null-safety\n`Chunk`/`MinBy`/`DistinctBy` | .NET 6 | Common LINQ patterns without manual loops\n`Index()` | C# 13 | Index-aware iteration without a counter\n`Span<T>` / `Memory<T>` | C# 7.2+ | Allocation-free slicing and parsing\nGeneric math | C# 11 | Numeric algorithms without per-type overloads\n`required` members | C# 11 | Enforced initialization without constructors\nCollection expressions | C# 12 | Unified, optimized collection syntax\n\n##  Conclusion\n\nModern C# has quietly become one of the more expressive and performance-conscious mainstream languages: you get the conciseness of records and pattern matching, the safety net of nullable reference types, and — when you need it — low-level control via `Span<T>` and generic math, all without leaving a garbage-collected, memory-safe runtime.\n\nThe common thread across every feature in this guide is that the language is optimizing for **both ends at once** : less code for the common case, and more control for the performance-critical case. That combination is why staying current with C# releases keeps paying off, even if you never touch a brand-new keyword directly — much of the runtime and BCL improvement happens under your feet.\n\n_Found this useful? Feel free to star the repo, open an issue with corrections, or share your own favorite modern C# feature._",
  "title": "Modern C# Features: A Deep Dive into Records, Pattern Matching, Async, and Performance"
}