{
"$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"
}