Modern C# (C# 14 / .NET 10)
Core Principles
- Use the newest stable features — C# 14 is the target. Prefer language-level constructs over library workarounds.
- Readability over cleverness — Pattern matching and expression-bodied members improve readability when used appropriately; deeply nested patterns do not.
- Value types where possible — Prefer
record struct, Span<T>, and stack allocation to reduce GC pressure.
- Immutability by default — Use
record, readonly, init, and required to make illegal states unrepresentable.
Patterns
Well-Known Features Quick Reference
| Feature |
Usage |
Example |
| Primary constructors |
DI injection, eliminate field assignments |
public class OrderService(IOrderRepo repo, TimeProvider clock) { } |
| Collection expressions |
[] for all collection types + spread |
List<string> names = ["Alice", "Bob"]; / int[] all = [..a, ..b, 99]; |
| Records |
DTOs, value objects, immutable data |
public record CreateOrderRequest(string CustomerId, List<OrderItem> Items); |
readonly record struct |
Small stack-allocated value types |
public readonly record struct Money(decimal Amount, string Currency); |
| Pattern matching |
Switch expressions, list/property patterns |
order switch { { Total: > 1000 } => "Premium", _ => "Standard" }; |
| List patterns |
Deconstruct arrays/lists |
items switch { [] => "Empty", [var x] => $"One: {x}", [var f, .., var l] => $"{f}..{l}" }; |
Span<T> |
Zero-allocation slicing |
ReadOnlySpan<char> trimmed = input.Trim(); int.TryParse(trimmed[4..], out id); |
| Raw string literals |
Multi-line SQL, JSON, XML |
var sql = """ SELECT ... """; / interpolated: $$""" {"id": "{{id}}"} """; |
required members |
Enforce initialization |
public required string ConnectionString { get; init; } |
is pattern + extraction |
Null/type/property check |
if (result is { IsSuccess: true, Value: var order }) { ... } |
The field Keyword (C# 14)
Access the auto-generated backing field in property accessors without declaring it manually.
// GOOD — field keyword for validation in auto-property
public class Product
{
public string Name
{
get => field;
set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
}
public decimal Price
{
get => field;
set => field = value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(value));
}
}
Lazy Initialization with field
public class ProductCatalog
{
// Lazy-load on first access — no manual Lazy<T> or backing field
public IReadOnlyList<Product> Products
{
get => field ??= LoadProducts();
}
private static List<Product> LoadProducts() => /* expensive load */;
}
Change Notification with field
// INotifyPropertyChanged without manual backing fields
public class OrderViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public string CustomerName
{
get => field;
set
{
if (field == value) return;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CustomerName)));
}
} = "";
public decimal Total
{
get => field;
set
{
if (field == value) return;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Total)));
}
}
}
Extension Members (C# 14)
C# 14 adds extension blocks inside static classes. Unlike classic extension methods, they support extension properties and static extension members — the receiver is declared once for the whole block.
// GOOD — extension block (shipped C# 14 syntax)
public static class OrderExtensions
{
extension(Order order)
{
public decimal TotalWithTax => order.Total * 1.2m;
public bool IsHighValue => order.Total > 1000m;
public string ToSummary() =>
$"Order #{order.Id}: {order.Total:C} ({order.Items.Count} items)";
}
// Static extension members use the type (no receiver instance)
extension(Order)
{
public static Order Empty => Order.Create("none", [], DateTimeOffset.MinValue);
}
}
// Callers see them as if declared on Order
if (order.IsHighValue) { /* ... */ }
Classic this-parameter extension methods still work and coexist — use extension blocks when you need properties or several members on the same receiver.
Anti-patterns
Don't Use Obsolete Patterns When Modern Alternatives Exist
// BAD — manual backing field when field keyword works
private string _name;
public string Name
{
get => _name;
set => _name = value ?? throw new ArgumentNullException();
}
// BAD — old-style collection initialization
var list = new List<int>() { 1, 2, 3 };
// BAD — Tuple instead of record for domain types
(string Name, decimal Price) product = ("Widget", 9.99m);
// GOOD — record
public record Product(string Name, decimal Price);
Don't Over-pattern-match
// BAD — deeply nested pattern that's hard to read
if (order is { Customer: { Address: { Country: { Code: "US" } } } })
// GOOD — extract to a clear method or use sequential checks
if (order.Customer.Address.Country.Code == "US")
Don't Use var When the Type Is Not Obvious
// BAD — what type is this?
var result = Process(order);
// GOOD — explicit type when not obvious
Result<Order> result = Process(order);
// Also GOOD — var is fine when type is apparent
var orders = new List<Order>();
Decision Guide
| Scenario |
Recommendation |
| DTO / API contract |
record (reference type) |
| Small value object (2-3 fields) |
readonly record struct |
| Service with DI |
Primary constructor |
| Collection creation |
Collection expression [] |
| Property with validation |
field keyword |
| Multi-line string (SQL, JSON) |
Raw string literal """ |
| Slicing strings/arrays |
Span<T> |
| Type checking + extraction |
Pattern matching with is / switch |
| Enforced initialization |
required modifier |
| Adding methods to external types |
Extension members |
1---
2name: modern-csharp
3description: >
4 Modern C# language features for .NET 10 and C# 14. Covers primary constructors,
5 collection expressions, the field keyword, extension members, records, pattern
6 matching, spans, and raw string literals.
7 Load this skill when writing any new C# code, reviewing existing code for
8 modernization, using "modern C#", "C# 14", "primary constructor", "collection
9 expression", "records", "pattern matching", "span", "field keyword", or
10 "extension members". Always loaded as the baseline for all agents.
11---
12
13# Modern C# (C# 14 / .NET 10)
14
15## Core Principles
16
171. **Use the newest stable features** — C# 14 is the target. Prefer language-level constructs over library workarounds.
182. **Readability over cleverness** — Pattern matching and expression-bodied members improve readability when used appropriately; deeply nested patterns do not.
193. **Value types where possible** — Prefer `record struct`, `Span<T>`, and stack allocation to reduce GC pressure.
204. **Immutability by default** — Use `record`, `readonly`, `init`, and `required` to make illegal states unrepresentable.
21
22## Patterns
23
24### Well-Known Features Quick Reference
25
26| Feature | Usage | Example |
27|---------|-------|---------|
28| Primary constructors | DI injection, eliminate field assignments | `public class OrderService(IOrderRepo repo, TimeProvider clock) { }` |
29| Collection expressions | `[]` for all collection types + spread | `List<string> names = ["Alice", "Bob"];` / `int[] all = [..a, ..b, 99];` |
30| Records | DTOs, value objects, immutable data | `public record CreateOrderRequest(string CustomerId, List<OrderItem> Items);` |
31| `readonly record struct` | Small stack-allocated value types | `public readonly record struct Money(decimal Amount, string Currency);` |
32| Pattern matching | Switch expressions, list/property patterns | `order switch { { Total: > 1000 } => "Premium", _ => "Standard" };` |
33| List patterns | Deconstruct arrays/lists | `items switch { [] => "Empty", [var x] => $"One: {x}", [var f, .., var l] => $"{f}..{l}" };` |
34| `Span<T>` | Zero-allocation slicing | `ReadOnlySpan<char> trimmed = input.Trim(); int.TryParse(trimmed[4..], out id);` |
35| Raw string literals | Multi-line SQL, JSON, XML | `var sql = """ SELECT ... """;` / interpolated: `$$""" {"id": "{{id}}"} """;` |
36| `required` members | Enforce initialization | `public required string ConnectionString { get; init; }` |
37| `is` pattern + extraction | Null/type/property check | `if (result is { IsSuccess: true, Value: var order }) { ... }` |
38
39### The `field` Keyword (C# 14)
40
41Access the auto-generated backing field in property accessors without declaring it manually.
42
43```csharp
44// GOOD — field keyword for validation in auto-property
45public class Product
46{
47 public string Name
48 {
49 get => field;
50 set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
51 }
52
53 public decimal Price
54 {
55 get => field;
56 set => field = value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(value));
57 }
58}
59```
60
61#### Lazy Initialization with `field`
62
63```csharp
64public class ProductCatalog
65{
66 // Lazy-load on first access — no manual Lazy<T> or backing field
67 public IReadOnlyList<Product> Products
68 {
69 get => field ??= LoadProducts();
70 }
71
72 private static List<Product> LoadProducts() => /* expensive load */;
73}
74```
75
76#### Change Notification with `field`
77
78```csharp
79// INotifyPropertyChanged without manual backing fields
80public class OrderViewModel : INotifyPropertyChanged
81{
82 public event PropertyChangedEventHandler? PropertyChanged;
83
84 public string CustomerName
85 {
86 get => field;
87 set
88 {
89 if (field == value) return;
90 field = value;
91 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CustomerName)));
92 }
93 } = "";
94
95 public decimal Total
96 {
97 get => field;
98 set
99 {
100 if (field == value) return;
101 field = value;
102 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Total)));
103 }
104 }
105}
106```
107
108### Extension Members (C# 14)
109
110C# 14 adds `extension` blocks inside static classes. Unlike classic extension methods, they support extension **properties** and **static** extension members — the receiver is declared once for the whole block.
111
112```csharp
113// GOOD — extension block (shipped C# 14 syntax)
114public static class OrderExtensions
115{
116 extension(Order order)
117 {
118 public decimal TotalWithTax => order.Total * 1.2m;
119
120 public bool IsHighValue => order.Total > 1000m;
121
122 public string ToSummary() =>
123 $"Order #{order.Id}: {order.Total:C} ({order.Items.Count} items)";
124 }
125
126 // Static extension members use the type (no receiver instance)
127 extension(Order)
128 {
129 public static Order Empty => Order.Create("none", [], DateTimeOffset.MinValue);
130 }
131}
132
133// Callers see them as if declared on Order
134if (order.IsHighValue) { /* ... */ }
135```
136
137Classic `this`-parameter extension methods still work and coexist — use extension blocks when you need properties or several members on the same receiver.
138
139## Anti-patterns
140
141### Don't Use Obsolete Patterns When Modern Alternatives Exist
142
143```csharp
144// BAD — manual backing field when field keyword works
145private string _name;
146public string Name
147{
148 get => _name;
149 set => _name = value ?? throw new ArgumentNullException();
150}
151
152// BAD — old-style collection initialization
153var list = new List<int>() { 1, 2, 3 };
154
155// BAD — Tuple instead of record for domain types
156(string Name, decimal Price) product = ("Widget", 9.99m);
157// GOOD — record
158public record Product(string Name, decimal Price);
159```
160
161### Don't Over-pattern-match
162
163```csharp
164// BAD — deeply nested pattern that's hard to read
165if (order is { Customer: { Address: { Country: { Code: "US" } } } })
166
167// GOOD — extract to a clear method or use sequential checks
168if (order.Customer.Address.Country.Code == "US")
169```
170
171### Don't Use `var` When the Type Is Not Obvious
172
173```csharp
174// BAD — what type is this?
175var result = Process(order);
176
177// GOOD — explicit type when not obvious
178Result<Order> result = Process(order);
179// Also GOOD — var is fine when type is apparent
180var orders = new List<Order>();
181```
182
183## Decision Guide
184
185| Scenario | Recommendation |
186|----------|---------------|
187| DTO / API contract | `record` (reference type) |
188| Small value object (2-3 fields) | `readonly record struct` |
189| Service with DI | Primary constructor |
190| Collection creation | Collection expression `[]` |
191| Property with validation | `field` keyword |
192| Multi-line string (SQL, JSON) | Raw string literal `"""` |
193| Slicing strings/arrays | `Span<T>` |
194| Type checking + extraction | Pattern matching with `is` / `switch` |
195| Enforced initialization | `required` modifier |
196| Adding methods to external types | Extension members |