$npx -y skills add wshaddix/dotnet-skills --skill dotnet-csharp-modern-patternsUsing records, pattern matching, primary constructors, collection expressions. C# 12-15 by TFM.
| 1 | # dotnet-csharp-modern-patterns |
| 2 | |
| 3 | Modern C# language feature guidance adapted to the project's target framework. Always run [skill:dotnet-version-detection] first to determine TFM and C# version. |
| 4 | |
| 5 | Cross-references: [skill:dotnet-csharp-coding-standards] for naming/style conventions, [skill:dotnet-csharp-async-patterns] for async-specific patterns. |
| 6 | |
| 7 | --- |
| 8 | |
| 9 | ## Quick Reference: TFM to C# Version |
| 10 | |
| 11 | | TFM | C# | Key Language Features | |
| 12 | |-----|----|-----------------------| |
| 13 | | net8.0 | 12 | Primary constructors, collection expressions, alias any type | |
| 14 | | net9.0 | 13 | `params` collections, `Lock` type, partial properties | |
| 15 | | net10.0 | 14 | `field` keyword, extension blocks, `nameof` unbound generics | |
| 16 | | net11.0 | 15 (preview) | Collection expression `with()` arguments | |
| 17 | |
| 18 | --- |
| 19 | |
| 20 | ## Records |
| 21 | |
| 22 | Use records for immutable data transfer objects, value semantics, and domain modeling where equality is based on values rather than identity. |
| 23 | |
| 24 | ### Record Classes (reference type) |
| 25 | |
| 26 | ```csharp |
| 27 | // Positional record: concise, immutable, value equality |
| 28 | public record OrderSummary(int OrderId, decimal Total, DateOnly OrderDate); |
| 29 | |
| 30 | // With additional members |
| 31 | public record Customer(string Name, string Email) |
| 32 | { |
| 33 | public string DisplayName => $"{Name} <{Email}>"; |
| 34 | } |
| 35 | ``` |
| 36 | |
| 37 | ### Record Structs (value type, C# 10+) |
| 38 | |
| 39 | ```csharp |
| 40 | // Positional record struct: value type with value semantics |
| 41 | public readonly record struct Point(double X, double Y); |
| 42 | |
| 43 | // Mutable record struct (rare -- prefer readonly) |
| 44 | public record struct MutablePoint(double X, double Y); |
| 45 | ``` |
| 46 | |
| 47 | ### When to Use Records vs Classes |
| 48 | |
| 49 | | Use Case | Prefer | |
| 50 | |----------|--------| |
| 51 | | DTOs, API responses | `record` | |
| 52 | | Domain value objects (Money, Email) | `readonly record struct` | |
| 53 | | Entities with identity (User, Order) | `class` | |
| 54 | | High-throughput, small data | `readonly record struct` | |
| 55 | | Inheritance needed | `record` (class-based) | |
| 56 | |
| 57 | ### Non-destructive Mutation |
| 58 | |
| 59 | ```csharp |
| 60 | var updated = order with { Total = order.Total + tax }; |
| 61 | ``` |
| 62 | |
| 63 | --- |
| 64 | |
| 65 | ## Primary Constructors (C# 12+, net8.0+) |
| 66 | |
| 67 | Capture constructor parameters directly in the class/struct body. Parameters become available throughout the type but are **not** fields or properties -- they are captured state. |
| 68 | |
| 69 | ### For Services (DI injection) |
| 70 | |
| 71 | ```csharp |
| 72 | public class OrderService(IOrderRepository repo, ILogger<OrderService> logger) |
| 73 | { |
| 74 | public async Task<Order> GetAsync(int id) |
| 75 | { |
| 76 | logger.LogInformation("Fetching order {OrderId}", id); |
| 77 | return await repo.GetByIdAsync(id); |
| 78 | } |
| 79 | } |
| 80 | ``` |
| 81 | |
| 82 | ### Gotchas |
| 83 | |
| 84 | - Primary constructor parameters are **mutable** captures, not `readonly` fields. If immutability matters, assign to a `readonly` field in the body. |
| 85 | - Do not use primary constructors when you need to validate parameters at construction time -- use a traditional constructor with guard clauses instead. |
| 86 | - For records, positional parameters become public properties automatically. For classes/structs, they remain private captures. |
| 87 | |
| 88 | ```csharp |
| 89 | // Explicit readonly field when immutability matters |
| 90 | public class Config(string connectionString) |
| 91 | { |
| 92 | private readonly string _connectionString = connectionString |
| 93 | ?? throw new ArgumentNullException(nameof(connectionString)); |
| 94 | } |
| 95 | ``` |
| 96 | |
| 97 | --- |
| 98 | |
| 99 | ## Collection Expressions (C# 12+, net8.0+) |
| 100 | |
| 101 | Unified syntax for creating collections with `[...]`. |
| 102 | |
| 103 | ```csharp |
| 104 | // Array |
| 105 | int[] numbers = [1, 2, 3]; |
| 106 | |
| 107 | // List |
| 108 | List<string> names = ["Alice", "Bob"]; |
| 109 | |
| 110 | // Span |
| 111 | ReadOnlySpan<byte> bytes = [0x00, 0xFF]; |
| 112 | |
| 113 | // Spread operator |
| 114 | int[] combined = [..first, ..second, 99]; |
| 115 | |
| 116 | // Empty collection |
| 117 | List<int> empty = []; |
| 118 | ``` |
| 119 | |
| 120 | ### Collection Expression with Arguments (C# 15 preview, net11.0+) |
| 121 | |
| 122 | Specify capacity, comparers, or other constructor arguments: |
| 123 | |
| 124 | ```csharp |
| 125 | // Capacity hint |
| 126 | List<int> nums = [with(capacity: 1000), ..Generate()]; |
| 127 | |
| 128 | // Custom comparer |
| 129 | HashSet<string> set = [with(comparer: StringComparer.OrdinalIgnoreCase), "Alice", "bob"]; |
| 130 | |
| 131 | // Dictionary with comparer |
| 132 | Dictionary<string, int> map = [with(comparer: StringComparer.OrdinalIgnoreCase), |
| 133 | new("key1", 1), new("key2", 2)]; |
| 134 | ``` |
| 135 | |
| 136 | > **net11.0+ only.** Requires `<LangVersion>preview</LangVersion>`. Do not use on earlier TFMs. |
| 137 | |
| 138 | --- |
| 139 | |
| 140 | ## Pattern Matching |
| 141 | |
| 142 | ### Switch Expressions (C# 8+) |
| 143 | |
| 144 | ```csharp |
| 145 | string GetDiscount(Customer customer) => customer switch |
| 146 | { |
| 147 | { Tier: "Gold", YearsActive: > 5 } => "30%", |
| 148 | { Tier: "Gold" } => "20%", |
| 149 | { Tier: "Silver" } => "10%", |
| 150 | _ => "0%" |
| 151 | }; |
| 152 | ``` |
| 153 | |
| 154 | ### List Patterns (C# 11+) |
| 155 | |
| 156 | ```csharp |
| 157 | bool IsValid(int[] data) => data is [> 0, .., > 0]; // first and last positive |
| 158 | |
| 159 | string Describe(int[] values) => values switch |
| 160 | { |
| 161 | [] => "empty", |
| 162 | [var single] => $"single: {single}", |
| 163 | [var first, .., var last] => $"range: {first}..{last}" |
| 164 | }; |
| 165 | ``` |
| 166 | |
| 167 | ### Type and Property Patterns |
| 168 | |
| 169 | ```csharp |
| 170 | decimal CalculateShipping(object package) => package switch |
| 171 | { |
| 172 | Letter { Weight: < 50 } => 0.50m, |
| 173 | Parcel { Weight: v |