dotnet-csharp-modern-patterns
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.
Scope
- Records, pattern matching, primary constructors, collection expressions
- C# 12-15 feature usage mapped to TFM
- Language feature adoption guidance
Out of scope
- Naming and style conventions -- see [skill:dotnet-csharp-coding-standards]
- Async/await patterns -- see [skill:dotnet-csharp-async-patterns]
- Source generator usage (GeneratedRegex, LoggerMessage) -- see [skill:dotnet-csharp-source-generators]
Cross-references: [skill:dotnet-csharp-coding-standards] for naming/style conventions,
[skill:dotnet-csharp-async-patterns] for async-specific patterns.
Quick Reference: TFM to C# Version
| TFM |
C# |
Key Language Features |
| net8.0 |
12 |
Primary constructors, collection expressions, alias any type |
| net9.0 |
13 |
params collections, Lock type, partial properties |
| net10.0 |
14 |
field keyword, extension blocks, nameof unbound generics |
| net11.0 |
15 (preview) |
Collection expression with() arguments |
Records
Use records for immutable data transfer objects, value semantics, and domain modeling where equality is based on values
rather than identity.
Record Classes (reference type)
// Positional record: concise, immutable, value equality
public record OrderSummary(int OrderId, decimal Total, DateOnly OrderDate);
// With additional members
public record Customer(string Name, string Email)
{
public string DisplayName => $"{Name} <{Email}>";
}
```text
### Record Structs (value type, C# 10+)
```csharp
// Positional record struct: value type with value semantics
public readonly record struct Point(double X, double Y);
// Mutable record struct (rare -- prefer readonly)
public record struct MutablePoint(double X, double Y);
```text
### When to Use Records vs Classes
| Use Case | Prefer |
| ------------------------------------ | ------------------------ |
| DTOs, API responses | `record` |
| Domain value objects (Money, Email) | `readonly record struct` |
| Entities with identity (User, Order) | `class` |
| High-throughput, small data | `readonly record struct` |
| Inheritance needed | `record` (class-based) |
### Non-destructive Mutation
```csharp
var updated = order with { Total = order.Total + tax };
```csharp
---
## Primary Constructors (C# 12+, net8.0+)
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.
### For Services (DI injection)
```csharp
public class OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
public async Task<Order> GetAsync(int id)
{
logger.LogInformation("Fetching order {OrderId}", id);
return await repo.GetByIdAsync(id);
}
}
```text
### Gotchas
- Primary constructor parameters are **mutable** captures, not `readonly` fields. If immutability matters, assign to a
`readonly` field in the body.
- Do not use primary constructors when you need to validate parameters at construction time -- use a traditional
constructor with guard clauses instead.
- For records, positional parameters become public properties automatically. For classes/structs, they remain private
captures.
```csharp
// Explicit readonly field when immutability matters
public class Config(string connectionString)
{
private readonly string _connectionString = connectionString
?? throw new ArgumentNullException(nameof(connectionString));
}
```text
---
## Collection Expressions (C# 12+, net8.0+)
Unified syntax for creating collections with `[...]`.
```csharp
// Array
int[] numbers = [1, 2, 3];
// List
List<string> names = ["Alice", "Bob"];
// Span
ReadOnlySpan<byte> bytes = [0x00, 0xFF];
// Spread operator
int[] combined = [..first, ..second, 99];
// Empty collection
List<int> empty = [];
```text
### Collection Expression with Arguments (C# 15 preview, net11.0+)
Specify capacity, comparers, or other constructor arguments:
```csharp
// Capacity hint
List<int> nums = [with(capacity: 1000), ..Generate()];
// Custom comparer
HashSet<string> set = [with(comparer: StringComparer.OrdinalIgnoreCase), "Alice", "bob"];
// Dictionary with comparer
Dictionary<string, int> map = [with(comparer: StringComparer.OrdinalIgnoreCase),
new("key1", 1), new("key2", 2)];
```text
> **net11.0+ only.** Requires `<LangVersion>preview</LangVersion>`. Do not use on earlier TFMs.
---
## Pattern Matching
### Switch Expressions (C# 8+)
```csharp
string GetDiscount(Customer customer) => customer switch
{
{ Tier: "Gold", YearsActive: > 5 } => "30%",
{ Tier: "Gold" } => "20%",
{ Tier: "Silver" } => "10%",
_ => "0%"
};
```text
### List Patterns (C# 11+)
```csharp
bool IsValid(int[] data) => data is [> 0, .., > 0]; // first and last positive
string Describe(int[] values) => values switch
{
[] => "empty",
[var single] => $"single: {single}",
[var first, .., var last] => $"range: {first}..{last}"
};
```text
### Type and Property Patterns
```csharp
decimal CalculateShipping(object package) => package switch
{
Letter { Weight: < 50 } => 0.50m,
Parcel { Weight: var w } when w < 1000 => 5.00m + w * 0.01m,
Parcel { IsOversized: true } => 25.00m,
_ => 10.00m
};
```text
---
## `required` Members (C# 11+)
Force callers to initialize properties at construction via object initializers.
```csharp
public class UserDto
{
public required string Name { get; init; }
public required string Email { get; init; }
public string? Phone { get; init; }
}
// Compiler enforces Name and Email
var user = new UserDto { Name = "Alice", Email = "alice@example.com" };
```text
Useful for DTOs that need to be deserialized (System.Text.Json honors `required` in .NET 8+).
---
## `field` Keyword (C# 14, net10.0+)
Access the compiler-generated backing field directly in property accessors.
```csharp
public class TemperatureSensor
{
public double Reading
{
get => field;
set => field = value >= -273.15
? value
: throw new ArgumentOutOfRangeException(nameof(value));
}
}
```text
Replaces the manual pattern of declaring a private field plus a property with custom logic. Use when you need validation
or transformation in a setter without a separate backing field.
> **net10.0+ only.** On earlier TFMs, use a traditional private field.
---
## Extension Blocks (C# 14, net10.0+)
Group extension members for a type in a single block.
```csharp
public static class EnumerableExtensions
{
extension<T>(IEnumerable<T> source) where T : class
{
public IEnumerable<T> WhereNotNull()
=> source.Where(x => x is not null);
public bool IsEmpty()
=> !source.Any();
}
}
```text
> **net10.0+ only.** On earlier TFMs, use traditional `static` extension methods.
---
## Alias Any Type (`using`, C# 12+, net8.0+)
```csharp
using Point = (double X, double Y);
using UserId = System.Guid;
Point origin = (0, 0);
UserId id = UserId.NewGuid();
```text
Useful for tuple aliases and domain type aliases without creating a full type.
---
## `params` Collections (C# 13, net9.0+)
`params` now supports additional collection types beyond arrays, including `Span<T>`, `ReadOnlySpan<T>`, and types
implementing certain collection interfaces.
```csharp
public void Log(params ReadOnlySpan<string> messages)
{
foreach (var msg in messages)
Console.WriteLine(msg);
}
// Callers: compiler may avoid heap allocation with span-based params
Log("hello", "world");
```text
> **net9.0+ only.** On net8.0, `params` only supports arrays.
---
## `Lock` Type (C# 13, net9.0+)
Use `System.Threading.Lock` instead of `object` for locking.
```csharp
private readonly Lock _lock = new();
public void DoWork()
{
lock (_lock)
{
// thread-safe operation
}
}
```text
`Lock` provides a `Scope`-based API for advanced scenarios and is more expressive than `lock (object)`.
> **net9.0+ only.** On net8.0, use `private readonly object _gate = new();` and `lock (_gate)`.
---
## Partial Properties (C# 13, net9.0+)
Partial properties enable source generators to define property signatures that users implement, or vice versa.
```csharp
// In generated file
public partial class ViewModel
{
public partial string Name { get; set; }
}
// In user file
public partial class ViewModel
{
private string _name = "";
## Detailed Examples
See [references/detailed-examples.md](references/detailed-examples.md) for complete code samples and advanced patterns.
1---2name: dotnet-csharp-modern-patterns3description: Using records, pattern matching, primary constructors, collection expressions. C# 12-15 by TFM.4license: MIT5targets: ['*']6category: fundamentals7subcategory: language-patterns8tags:9 - csharp10 - dotnet11 - skill12 - language-patterns13 - records14version: '1.0.0'15author: 'dotnet-agent-harness'16invocable: true17related_skills:18 - dotnet-csharp-coding-standards19 - dotnet-csharp-async-patterns20 - dotnet-10-csharp-1421claudecode:22 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']23codexcli:24 short-description: '.NET skill guidance for csharp tasks'25opencode:26 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']27copilot: {}28geminicli: {}29antigravity: {}30---3132# dotnet-csharp-modern-patterns3334Modern C# language feature guidance adapted to the project's target framework. Always run35[skill:dotnet-version-detection] first to determine TFM and C# version.3637## Scope3839- Records, pattern matching, primary constructors, collection expressions40- C# 12-15 feature usage mapped to TFM41- Language feature adoption guidance4243## Out of scope4445- Naming and style conventions -- see [skill:dotnet-csharp-coding-standards]46- Async/await patterns -- see [skill:dotnet-csharp-async-patterns]47- Source generator usage (GeneratedRegex, LoggerMessage) -- see [skill:dotnet-csharp-source-generators]4849Cross-references: [skill:dotnet-csharp-coding-standards] for naming/style conventions,50[skill:dotnet-csharp-async-patterns] for async-specific patterns.5152---5354## Quick Reference: TFM to C# Version5556| TFM | C# | Key Language Features |57| ------- | ------------ | ------------------------------------------------------------ |58| net8.0 | 12 | Primary constructors, collection expressions, alias any type |59| net9.0 | 13 | `params` collections, `Lock` type, partial properties |60| net10.0 | 14 | `field` keyword, extension blocks, `nameof` unbound generics |61| net11.0 | 15 (preview) | Collection expression `with()` arguments |6263---6465## Records6667Use records for immutable data transfer objects, value semantics, and domain modeling where equality is based on values68rather than identity.6970### Record Classes (reference type)7172````csharp7374// Positional record: concise, immutable, value equality75public record OrderSummary(int OrderId, decimal Total, DateOnly OrderDate);7677// With additional members78public record Customer(string Name, string Email)79{80 public string DisplayName => $"{Name} <{Email}>";81}8283```text8485### Record Structs (value type, C# 10+)8687```csharp8889// Positional record struct: value type with value semantics90public readonly record struct Point(double X, double Y);9192// Mutable record struct (rare -- prefer readonly)93public record struct MutablePoint(double X, double Y);9495```text9697### When to Use Records vs Classes9899| Use Case | Prefer |100| ------------------------------------ | ------------------------ |101| DTOs, API responses | `record` |102| Domain value objects (Money, Email) | `readonly record struct` |103| Entities with identity (User, Order) | `class` |104| High-throughput, small data | `readonly record struct` |105| Inheritance needed | `record` (class-based) |106107### Non-destructive Mutation108109```csharp110111var updated = order with { Total = order.Total + tax };112113```csharp114115---116117## Primary Constructors (C# 12+, net8.0+)118119Capture constructor parameters directly in the class/struct body. Parameters become available throughout the type but120are **not** fields or properties -- they are captured state.121122### For Services (DI injection)123124```csharp125126public class OrderService(IOrderRepository repo, ILogger<OrderService> logger)127{128 public async Task<Order> GetAsync(int id)129 {130 logger.LogInformation("Fetching order {OrderId}", id);131 return await repo.GetByIdAsync(id);132 }133}134135```text136137### Gotchas138139- Primary constructor parameters are **mutable** captures, not `readonly` fields. If immutability matters, assign to a140 `readonly` field in the body.141- Do not use primary constructors when you need to validate parameters at construction time -- use a traditional142 constructor with guard clauses instead.143- For records, positional parameters become public properties automatically. For classes/structs, they remain private144 captures.145146```csharp147148// Explicit readonly field when immutability matters149public class Config(string connectionString)150{151 private readonly string _connectionString = connectionString152 ?? throw new ArgumentNullException(nameof(connectionString));153}154155```text156157---158159## Collection Expressions (C# 12+, net8.0+)160161Unified syntax for creating collections with `[...]`.162163```csharp164165// Array166int[] numbers = [1, 2, 3];167168// List169List<string> names = ["Alice", "Bob"];170171// Span172ReadOnlySpan<byte> bytes = [0x00, 0xFF];173174// Spread operator175int[] combined = [..first, ..second, 99];176177// Empty collection178List<int> empty = [];179180```text181182### Collection Expression with Arguments (C# 15 preview, net11.0+)183184Specify capacity, comparers, or other constructor arguments:185186```csharp187188// Capacity hint189List<int> nums = [with(capacity: 1000), ..Generate()];190191// Custom comparer192HashSet<string> set = [with(comparer: StringComparer.OrdinalIgnoreCase), "Alice", "bob"];193194// Dictionary with comparer195Dictionary<string, int> map = [with(comparer: StringComparer.OrdinalIgnoreCase),196 new("key1", 1), new("key2", 2)];197198```text199200> **net11.0+ only.** Requires `<LangVersion>preview</LangVersion>`. Do not use on earlier TFMs.201202---203204## Pattern Matching205206### Switch Expressions (C# 8+)207208```csharp209210string GetDiscount(Customer customer) => customer switch211{212 { Tier: "Gold", YearsActive: > 5 } => "30%",213 { Tier: "Gold" } => "20%",214 { Tier: "Silver" } => "10%",215 _ => "0%"216};217218```text219220### List Patterns (C# 11+)221222```csharp223224bool IsValid(int[] data) => data is [> 0, .., > 0]; // first and last positive225226string Describe(int[] values) => values switch227{228 [] => "empty",229 [var single] => $"single: {single}",230 [var first, .., var last] => $"range: {first}..{last}"231};232233```text234235### Type and Property Patterns236237```csharp238239decimal CalculateShipping(object package) => package switch240{241 Letter { Weight: < 50 } => 0.50m,242 Parcel { Weight: var w } when w < 1000 => 5.00m + w * 0.01m,243 Parcel { IsOversized: true } => 25.00m,244 _ => 10.00m245};246247```text248249---250251## `required` Members (C# 11+)252253Force callers to initialize properties at construction via object initializers.254255```csharp256257public class UserDto258{259 public required string Name { get; init; }260 public required string Email { get; init; }261 public string? Phone { get; init; }262}263264// Compiler enforces Name and Email265var user = new UserDto { Name = "Alice", Email = "alice@example.com" };266267```text268269Useful for DTOs that need to be deserialized (System.Text.Json honors `required` in .NET 8+).270271---272273## `field` Keyword (C# 14, net10.0+)274275Access the compiler-generated backing field directly in property accessors.276277```csharp278279public class TemperatureSensor280{281 public double Reading282 {283 get => field;284 set => field = value >= -273.15285 ? value286 : throw new ArgumentOutOfRangeException(nameof(value));287 }288}289290```text291292Replaces the manual pattern of declaring a private field plus a property with custom logic. Use when you need validation293or transformation in a setter without a separate backing field.294295> **net10.0+ only.** On earlier TFMs, use a traditional private field.296297---298299## Extension Blocks (C# 14, net10.0+)300301Group extension members for a type in a single block.302303```csharp304305public static class EnumerableExtensions306{307 extension<T>(IEnumerable<T> source) where T : class308 {309 public IEnumerable<T> WhereNotNull()310 => source.Where(x => x is not null);311312 public bool IsEmpty()313 => !source.Any();314 }315}316317```text318319> **net10.0+ only.** On earlier TFMs, use traditional `static` extension methods.320321---322323## Alias Any Type (`using`, C# 12+, net8.0+)324325```csharp326327using Point = (double X, double Y);328using UserId = System.Guid;329330Point origin = (0, 0);331UserId id = UserId.NewGuid();332333```text334335Useful for tuple aliases and domain type aliases without creating a full type.336337---338339## `params` Collections (C# 13, net9.0+)340341`params` now supports additional collection types beyond arrays, including `Span<T>`, `ReadOnlySpan<T>`, and types342implementing certain collection interfaces.343344```csharp345346public void Log(params ReadOnlySpan<string> messages)347{348 foreach (var msg in messages)349 Console.WriteLine(msg);350}351352// Callers: compiler may avoid heap allocation with span-based params353Log("hello", "world");354355```text356357> **net9.0+ only.** On net8.0, `params` only supports arrays.358359---360361## `Lock` Type (C# 13, net9.0+)362363Use `System.Threading.Lock` instead of `object` for locking.364365```csharp366367private readonly Lock _lock = new();368369public void DoWork()370{371 lock (_lock)372 {373 // thread-safe operation374 }375}376377```text378379`Lock` provides a `Scope`-based API for advanced scenarios and is more expressive than `lock (object)`.380381> **net9.0+ only.** On net8.0, use `private readonly object _gate = new();` and `lock (_gate)`.382383---384385## Partial Properties (C# 13, net9.0+)386387Partial properties enable source generators to define property signatures that users implement, or vice versa.388389```csharp390391// In generated file392public partial class ViewModel393{394 public partial string Name { get; set; }395}396397// In user file398public partial class ViewModel399{400 private string _name = "";401402## Detailed Examples403404See [references/detailed-examples.md](references/detailed-examples.md) for complete code samples and advanced patterns.