- name
- csharp-best-practices
- description
- C# coding conventions, idiomatic patterns, performance, and async best practices for both .NET 8 (C# 12) and .NET 10 (C# 14). Use when writing, reviewing, refactoring, or designing C# code — including async code that uses Task, generic Task results, ValueTask, CancellationToken, Task.WhenAll/WhenAny, Task.Run, ConfigureAwait, async void, or fire-and-forget. Trigger on `.Result`, `.Wait()`, deadlocks, cancellation propagation, ASP.NET Core background work, UI responsiveness, exception flow, and performance-sensitive async API design.
- metadata
- {"category":"technique","platform":".NET 8 (C# 12) and .NET 10 (C# 14)","triggers":["c#","csharp",".net",".net 8",".net 10","async","task","valuetask","cancellationtoken","configureawait",".result",".wait()","async void","fire-and-forget","task.run","whenall","whenany","asp.net core","deadlock"]}
# C# Best Practices — .NET 8 + .NET 10
Target this repository's **.NET 10 / C# 14** projects with nullable reference types enabled.
## Step 0 — Detect the target framework
Before emitting code, inspect `Directory.Build.props` and the target project. The detected target framework decides which examples below apply:
- `net8.0` → emit the **.NET 8 / C# 12** code block in every side-by-side pair; **never** use the C# 14-only syntax (`field`, `extension(...)`, `?.` assignment, partial constructors) or .NET 10-only APIs (`HybridCache`, `AddValidation`, EF Core named filters, first-party `Microsoft.AspNetCore.OpenApi`, Identity passkeys).
- `net10.0` → prefer the **.NET 10 / C# 14** code block.
- Multi-target (`<TargetFrameworks>net8.0;net10.0</TargetFrameworks>`) → emit the .NET 8 version, or wrap .NET 10-only code in `#if NET10_0_OR_GREATER`.
- Unknown → ask the user.
Sources: [MS C# Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions) · [.NET Runtime Style](https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md) · [C# language versioning](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-versioning) · [C# 14 What's New](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-14) · [C# 12 What's New](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12)
## Naming
| Element | Style | Example |
|---|---|---|
| Type, method, property, const, enum member | PascalCase | `PacketHandler`, `MaxRetries`, `GameMode.Survival` |
| Interface | `I` + PascalCase | `IChatBot` |
| Private instance field | `_camelCase` | `_handler` |
| Private static field | `s_camelCase` | `s_defaultTimeout` |
| Thread-static field | `t_camelCase` | `t_cachedBuffer` |
| Local, parameter | camelCase | `packetId` |
| Type parameter | `T` + PascalCase | `TResult` |
| Namespace | PascalCase | `SomeNamespace.SomeClasses` |
| Async methods | Suffix `Async` | `ConnectAsync()`, `ReadPacketAsync()` |
```csharp
// CORRECT: naming conventions
private readonly Dictionary<int, Entity> _entities = new();
private static readonly TimeSpan s_reconnectDelay = TimeSpan.FromSeconds(5);
public int PacketCount { get; private set; }
public async Task<bool> ConnectAsync(CancellationToken ct) { }
```
```csharp
// WRONG: naming violations
private Dictionary<int, Entity> entities = new(); // missing _
private static TimeSpan reconnectDelay; // missing s_
public int packet_count { get; set; } // snake_case
public async Task<bool> Connect(CancellationToken ct) { } // missing Async suffix
```
## C# 14 Features (.NET 10 only — with .NET 8 / C# 12 fallbacks)
Every feature in this section requires `<TargetFramework>net10.0</TargetFramework>`. On `net8.0` use the fallback shown alongside. Authoritative reference: [C# 14 what's new](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-14).
### Extension Members
Declare extension methods, properties, and operators inside `extension(...)` blocks.
```csharp
// .NET 10 / C# 14 — extension property + method
public static class EntityExtensions
{
extension(Entity entity)
{
public bool IsAlive => entity.Health > 0;
public void Heal(int amount) => entity.Health = Math.Min(entity.Health + amount, 20);
}
extension<T>(IEnumerable<T> items)
{
public bool IsEmpty => !items.GetEnumerator().MoveNext();
}
}
```
```csharp
// .NET 8 / C# 12 — classic static extension class (only option)
public static class EntityExtensions
{
public static bool IsAlive(this Entity entity) => entity.Health > 0;
public static void Heal(this Entity entity, int amount)
=> entity.Health = Math.Min(entity.Health + amount, 20);
public static bool IsEmpty<T>(this IEnumerable<T> items)
=> !items.GetEnumerator().MoveNext();
}
// Extension properties do not exist in C# 12 — expose them as methods or compute inline.
```
### `field` Keyword in Properties
Access the auto-generated backing field without declaring it.
```csharp
// .NET 10 / C# 14 — field keyword
public string DisplayName => field ??= ComputeDisplayName();
public bool IsConnected
{
get;
set
{
if (field == value) return;
field = value;
OnPropertyChanged();
}
}
```
```csharp
// .NET 8 / C# 12 — manual backing field (only option)
private string? _displayName;
public string DisplayName => _displayName ??= ComputeDisplayName();
private bool _isConnected;
public bool IsConnected
{
get => _isConnected;
set
{
if (_isConnected == value) return;
_isConnected = value;
OnPropertyChanged();
}
}
```
### Null-Conditional Assignment
```csharp
// .NET 10 / C# 14 — assign / compound-assign through ?.
player?.Health = 20;
connection?.OnDisconnect += HandleDisconnect;
inventory?[slot] = newItem;
```
```csharp
// .NET 8 / C# 12 — manual null check
if (player is not null) player.Health = 20;
if (connection is not null) connection.OnDisconnect += HandleDisconnect;
if (inventory is not null) inventory[slot] = newItem;
```
### Simple Lambda Parameters with Modifiers
```csharp
// .NET 10 / C# 14 — modifiers without explicit types
TryParse<int> parse = (text, out result) => int.TryParse(text, out result);
ProcessSpan((scoped span) => span.Length);
```
```csharp
// .NET 8 / C# 12 — full parameter types required when modifiers are present
TryParse<int> parse = (string text, out int result) => int.TryParse(text, out result);
ProcessSpan((scoped ReadOnlySpan<int> span) => span.Length);
```
### First-Class Span Types
```csharp
// .NET 10 / C# 14 — implicit T[] → ReadOnlySpan<T>
int[] data = [1, 2, 3];
bool found = data.StartsWith(1); // ReadOnlySpan<int> extension auto-resolves
ReadOnlySpan<byte> span = stackalloc byte[4];
```
```csharp
// .NET 8 / C# 12 — call .AsSpan() explicitly at the boundary
int[] data = [1, 2, 3];
bool found = data.AsSpan().StartsWith(stackalloc int[] { 1 }); // explicit conversion
ReadOnlySpan<byte> span = stackalloc byte[4]; // stackalloc → ReadOnlySpan already works
```
### Unbound Generics in `nameof`
```csharp
// .NET 10 / C# 14
string name = nameof(Dictionary<,>); // "Dictionary"
string prop = nameof(List<>.Count); // "Count"
```
```csharp
// .NET 8 / C# 12 — pick any closed type argument
string name = nameof(Dictionary<object, object>); // "Dictionary"
string prop = nameof(List<int>.Count); // "Count"
```
### Partial Events and Constructors
```csharp
// .NET 10 / C# 14 — partial constructor for source-gen interop
partial class ServerConnection
{
partial ServerConnection(string host, int port);
}
partial class ServerConnection
{
partial ServerConnection(string host, int port) { /* generated body */ }
}
```
```csharp
// .NET 8 / C# 12 — partial constructors do not exist.
// Either declare a regular constructor and call a private generated helper,
// or put the constructor body in a single file:
partial class ServerConnection
{
public ServerConnection(string host, int port) => InitGenerated(host, port);
private partial void InitGenerated(string host, int port); // partial methods are C# 9+
}
partial class ServerConnection
{
private partial void InitGenerated(string host, int port) { /* generated body */ }
}
```
### `#:` Ignored Directives / file-based programs
C# 14 / .NET 10 SDK only — no .NET 8 equivalent. `dotnet run app.cs` requires the .NET 10 SDK.
```csharp
// .NET 10 only — file-based program with inline package reference
#!/usr/bin/dotnet run
#:package System.CommandLine@2.0.0-*
Console.WriteLine("Hello");
```
```csharp
// .NET 8 — create a full project (dotnet new console -f net8.0) and reference
// System.CommandLine in the .csproj. There is no inline-package syntax.
```
## C# 13 Features — require .NET 9+ (NOT available on .NET 8)
This repository targets .NET 10. Use the .NET 8 fallback only when reviewing external multi-targeted code.
### `Lock` Object
```csharp
// .NET 10 / C# 13+ — dedicated System.Threading.Lock type
private readonly Lock _gate = new();
public void Enqueue(ChatMessage msg) { lock (_gate) _queue.Add(msg); }
```
```csharp
// .NET 8 / C# 12 — lock on a plain object reference (the only option)
private readonly object _gate = new();
public void Enqueue(ChatMessage msg) { lock (_gate) _queue.Add(msg); }
```
### `params` Collections (`params ReadOnlySpan<T>`)
The runtime overloads accepting `params ReadOnlySpan<T>` ship in the .NET 9 BCL. On .NET 8 use `params T[]`.
```csharp
// .NET 10 / C# 13+ — params span avoids the array allocation
public void Log(params ReadOnlySpan<string> messages)
{
foreach (var msg in messages) Console.WriteLine(msg);
}
```
```csharp
// .NET 8 / C# 12 — params array (one heap allocation per call)
public void Log(params string[] messages)
{
foreach (var msg in messages) Console.WriteLine(msg);
}
```
### Partial Properties
```csharp
// .NET 10 / C# 13+ — partial property for source generators
partial class Config
{
public partial string Host { get; set; }
}
partial class Config
{
public partial string Host { get => _host; set => _host = value; }
private string _host = "";
}
```
```csharp
// .NET 8 / C# 12 — partial properties do not exist; declare a normal property
// and let the source generator emit the backing field or a helper method.
partial class Config
{
public string Host { get; set; } = "";
}
```
## C# 12 Features (.NET 8+ — available on both targets)
### Primary Constructors
Use for simple parameter capture. Parameters are `camelCase`, mutable — assign to `readonly` fields when immutability matters.
```csharp
// CORRECT: primary constructor captures dependencies
public class ChatLogger(string logFilePath, bool appendMode) : ChatBot
{
private readonly StreamWriter _writer = new(logFilePath, appendMode);
public override void GetText(string text) => _writer.WriteLine(text);
}
```
```csharp
// WRONG: verbose constructor boilerplate for simple capture
public class ChatLogger : ChatBot
{
private readonly StreamWriter _writer;
public ChatLogger(string logFilePath, bool appendMode)
{
_writer = new StreamWriter(logFilePath, appendMode);
}
public override void GetText(string text) => _writer.WriteLine(text);
}
```
### Collection Expressions
Use `[...]` and `..` spread for arrays, lists, spans.
```csharp
// CORRECT: collection expressions (C# 12)
int[] ids = [1, 2, 3];
List<string> names = ["Steve", "Alex"];
ReadOnlySpan<byte> header = [0xFE, 0x01]; // no heap alloc
int[] combined = [..firstArray, ..secondArray, 42];
IReadOnlyList<string> empty = [];
```
```csharp
// WRONG: verbose initialization
int[] ids = new int[] { 1, 2, 3 };
var names = new List<string> { "Steve", "Alex" };
ReadOnlySpan<byte> header = new byte[] { 0xFE, 0x01 }; // allocates
var combined = firstArray.Concat(secondArray).Append(42).ToArray();
```
### Type Aliases
```csharp
// CORRECT: alias complex types for readability
using Coordinate = (int X, int Y, int Z);
using PacketMap = System.Collections.Generic.Dictionary<int, System.Action<byte[]>>;
```
### Default Lambda Parameters
```csharp
// CORRECT: C# 12
var greet = (string name, string prefix = "Player") => $"{prefix} {name}";
```
## Modern Syntax (C# 10–14)
### File-Scoped Namespaces
```csharp
// CORRECT: file-scoped namespace — one per file, less nesting
namespace SomeNamespace.SomeClasses;
public class SomeClass : SomeInterface { }
```
```csharp
// WRONG: block-scoped namespace adds unnecessary nesting
namespace SomeNamespace.SomeClasses
{
public class SomeClass : SomeInterface { }
}
```
### Target-Typed `new`
Use when the type is obvious from the left-hand side.
GitHubで見る