mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Updated skills
This commit is contained in:
parent
c23a71c489
commit
3c452dfcc5
29 changed files with 3208 additions and 1550 deletions
|
|
@ -1,14 +1,46 @@
|
|||
---
|
||||
name: csharp-best-practices
|
||||
name: dotnet-csharp-best-practices
|
||||
description: >
|
||||
C# 14 / .NET 10 coding conventions, idiomatic patterns, and performance best practices
|
||||
for the Minecraft Console Client codebase. Use when writing, reviewing, or modifying C# code.
|
||||
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, Task<T>, 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# 14 / .NET 10 Best Practices
|
||||
# C# Best Practices — .NET 8 + .NET 10
|
||||
|
||||
Target: **.NET 10**, **C# 14**, nullable enabled.
|
||||
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# 14 Proposals](https://github.com/dotnet/csharplang/blob/main/Language-Version-History.md) · [C# 13 Docs](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-13)
|
||||
Target: **.NET 8 (C# 12)** *and* **.NET 10 (C# 14)**, nullable enabled. GrECo has no .NET 9 projects.
|
||||
|
||||
## Step 0 — Detect the target framework
|
||||
|
||||
**Before** emitting code, follow `../../references/detect-target-framework.md`. The detection result 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
|
||||
|
||||
|
|
@ -21,7 +53,7 @@ Sources: [MS C# Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fun
|
|||
| Thread-static field | `t_camelCase` | `t_cachedBuffer` |
|
||||
| Local, parameter | camelCase | `packetId` |
|
||||
| Type parameter | `T` + PascalCase | `TResult` |
|
||||
| Namespace | PascalCase | `MinecraftClient.Protocol` |
|
||||
| Namespace | PascalCase | `SomeNamespace.SomeClasses` |
|
||||
| Async methods | Suffix `Async` | `ConnectAsync()`, `ReadPacketAsync()` |
|
||||
|
||||
```csharp
|
||||
|
|
@ -40,14 +72,16 @@ public int packet_count { get; set; } // snake_case
|
|||
public async Task<bool> Connect(CancellationToken ct) { } // missing Async suffix
|
||||
```
|
||||
|
||||
## C# 14 Features
|
||||
## C# 14 Features (.NET 10 only — with .NET 8 / C# 12 fallbacks)
|
||||
|
||||
### Extension Members (C# 14)
|
||||
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).
|
||||
|
||||
Declare extension methods, properties, and operators inside `extension(...)` blocks. Replaces `this`-parameter pattern for new extensions.
|
||||
### Extension Members
|
||||
|
||||
Declare extension methods, properties, and operators inside `extension(...)` blocks.
|
||||
|
||||
```csharp
|
||||
// CORRECT: extension property + method (C# 14)
|
||||
// .NET 10 / C# 14 — extension property + method
|
||||
public static class EntityExtensions
|
||||
{
|
||||
extension(Entity entity)
|
||||
|
|
@ -63,19 +97,27 @@ public static class EntityExtensions
|
|||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: classic extension method when C# 14 extension block is available
|
||||
// .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 (C# 14)
|
||||
### `field` Keyword in Properties
|
||||
|
||||
Access the auto-generated backing field without declaring it. Mix auto and full accessors.
|
||||
Access the auto-generated backing field without declaring it.
|
||||
|
||||
```csharp
|
||||
// CORRECT: lazy init with field keyword
|
||||
// .NET 10 / C# 14 — field keyword
|
||||
public string DisplayName => field ??= ComputeDisplayName();
|
||||
|
||||
// CORRECT: INotifyPropertyChanged pattern
|
||||
public bool IsConnected
|
||||
{
|
||||
get;
|
||||
|
|
@ -89,128 +131,170 @@ public bool IsConnected
|
|||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: manual backing field when field keyword suffices
|
||||
// .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 (C# 14)
|
||||
|
||||
Assign through `?.` — RHS is only evaluated when receiver is non-null.
|
||||
### Null-Conditional Assignment
|
||||
|
||||
```csharp
|
||||
// CORRECT: null-conditional assignment
|
||||
// .NET 10 / C# 14 — assign / compound-assign through ?.
|
||||
player?.Health = 20;
|
||||
connection?.OnDisconnect += HandleDisconnect;
|
||||
inventory?[slot] = newItem;
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: manual null check for simple assignment
|
||||
if (player is not null)
|
||||
player.Health = 20;
|
||||
// .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 (C# 14)
|
||||
|
||||
Omit types on lambda parameters while still applying modifiers.
|
||||
### Simple Lambda Parameters with Modifiers
|
||||
|
||||
```csharp
|
||||
// CORRECT: modifiers without explicit types
|
||||
// .NET 10 / C# 14 — modifiers without explicit types
|
||||
TryParse<int> parse = (text, out result) => int.TryParse(text, out result);
|
||||
ReadOnlySpan<int> data = [1, 2, 3];
|
||||
ProcessSpan((scoped span) => span.Length);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: fully explicit types just for a modifier
|
||||
// .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 (C# 14)
|
||||
|
||||
Implicit conversions between `T[]`, `Span<T>`, and `ReadOnlySpan<T>` — no explicit cast needed. Extension methods on `ReadOnlySpan<T>` apply to arrays and spans automatically.
|
||||
### First-Class Span Types
|
||||
|
||||
```csharp
|
||||
// CORRECT: pass array where ReadOnlySpan<T> is expected (C# 14)
|
||||
// .NET 10 / C# 14 — implicit T[] → ReadOnlySpan<T>
|
||||
int[] data = [1, 2, 3];
|
||||
bool found = data.StartsWith(1); // ReadOnlySpan<int> extension resolved
|
||||
bool found = data.StartsWith(1); // ReadOnlySpan<int> extension auto-resolves
|
||||
ReadOnlySpan<byte> span = stackalloc byte[4];
|
||||
```
|
||||
|
||||
### Unbound Generics in `nameof` (C# 14)
|
||||
```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
|
||||
// CORRECT: no need to pick a dummy type argument
|
||||
// .NET 10 / C# 14
|
||||
string name = nameof(Dictionary<,>); // "Dictionary"
|
||||
string prop = nameof(List<>.Count); // "Count"
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: arbitrary type argument just to satisfy nameof
|
||||
string name = nameof(Dictionary<object, object>);
|
||||
// .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 (C# 14)
|
||||
|
||||
Separate declaration from implementation for source-generator scenarios.
|
||||
### Partial Events and Constructors
|
||||
|
||||
```csharp
|
||||
// CORRECT: partial constructor for source-gen interop
|
||||
// .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 */ }
|
||||
partial ServerConnection(string host, int port) { /* generated body */ }
|
||||
}
|
||||
```
|
||||
|
||||
### `#:` Ignored Directives (C# 14)
|
||||
```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 */ }
|
||||
}
|
||||
```
|
||||
|
||||
For file-based `dotnet run app.cs` programs — ignored by the compiler.
|
||||
### `#:` 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");
|
||||
```
|
||||
|
||||
## C# 13 Features
|
||||
```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.
|
||||
```
|
||||
|
||||
### `Lock` Object (C# 13)
|
||||
## C# 13 Features — require .NET 9+ (NOT available on .NET 8)
|
||||
|
||||
Use `System.Threading.Lock` instead of `lock(obj)` on arbitrary objects.
|
||||
GrECo has no .NET 9 projects, so the only way to use C# 13 features in production is to be on .NET 10. On .NET 8, use the .NET 8 fallback shown below.
|
||||
|
||||
### `Lock` Object
|
||||
|
||||
```csharp
|
||||
// CORRECT: dedicated Lock type
|
||||
private readonly Lock _lock = new();
|
||||
public void Enqueue(ChatMessage msg) { lock (_lock) _queue.Add(msg); }
|
||||
// .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
|
||||
// WRONG: locking on an object reference
|
||||
private readonly object _syncRoot = new();
|
||||
lock (_syncRoot) { }
|
||||
// .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 (C# 13)
|
||||
### `params` Collections (`params ReadOnlySpan<T>`)
|
||||
|
||||
`params` now works with `ReadOnlySpan<T>`, `Span<T>`, `IEnumerable<T>`, and other collection types.
|
||||
The runtime overloads accepting `params ReadOnlySpan<T>` ship in the .NET 9 BCL. On .NET 8 use `params T[]`.
|
||||
|
||||
```csharp
|
||||
// CORRECT: params span avoids array allocation
|
||||
// .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);
|
||||
}
|
||||
```
|
||||
|
||||
### Partial Properties (C# 13)
|
||||
```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
|
||||
// CORRECT: partial property for source generators
|
||||
// .NET 10 / C# 13+ — partial property for source generators
|
||||
partial class Config
|
||||
{
|
||||
public partial string Host { get; set; }
|
||||
|
|
@ -222,7 +306,16 @@ partial class Config
|
|||
}
|
||||
```
|
||||
|
||||
## C# 12 Features
|
||||
```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
|
||||
|
||||
|
|
@ -292,16 +385,16 @@ var greet = (string name, string prefix = "Player") => $"{prefix} {name}";
|
|||
|
||||
```csharp
|
||||
// CORRECT: file-scoped namespace — one per file, less nesting
|
||||
namespace MinecraftClient.ChatBots;
|
||||
namespace SomeNamespace.SomeClasses;
|
||||
|
||||
public class MyBot : ChatBot { }
|
||||
public class SomeClass : SomeInterface { }
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: block-scoped namespace adds unnecessary nesting
|
||||
namespace MinecraftClient.ChatBots
|
||||
namespace SomeNamespace.SomeClasses
|
||||
{
|
||||
public class MyBot : ChatBot { }
|
||||
public class SomeClass : SomeInterface { }
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -512,6 +605,34 @@ public string GetName(Player? player)
|
|||
|
||||
## Async / Await
|
||||
|
||||
<priority-order>
|
||||
1. correctness and cancellation semantics
|
||||
2. context-specific API design (library vs app, UI vs ASP.NET Core)
|
||||
3. concurrency behavior and failure handling
|
||||
4. performance tuning only when the hot path is real
|
||||
</priority-order>
|
||||
|
||||
Treat blanket advice as suspect. Separate official runtime behavior from expert interpretation and from your own recommendation for the case at hand.
|
||||
|
||||
### Review defaults
|
||||
|
||||
Start from these defaults unless case-specific evidence says otherwise:
|
||||
|
||||
| Topic | Default judgment |
|
||||
|---|---|
|
||||
| Blocking on async (`.Result`, `.Wait`, `GetAwaiter().GetResult()`) | usually a defect or interop boundary smell |
|
||||
| `async void` | only acceptable for event handlers |
|
||||
| `ValueTask` | avoid by default; justify with measurements or a very hot path |
|
||||
| `ConfigureAwait(false)` | good library default, not an app-wide default |
|
||||
| `Task.Run` | use to offload CPU work when needed, not to fake async I/O |
|
||||
| Fire-and-forget | assume unsafe until lifecycle, scope, and exception handling are explicit |
|
||||
| `Task.WhenAll` | prefer for independent concurrent operations |
|
||||
| `Task.WhenAny` | always inspect the winner task and define what happens to losers |
|
||||
| Cancellation | accept and propagate the token until the point of no cancellation |
|
||||
| Method naming | `Async` suffix for awaitable-returning methods (unless an interface/event contract dictates otherwise) |
|
||||
|
||||
### Concrete patterns
|
||||
|
||||
```csharp
|
||||
// CORRECT: propagate CancellationToken through every async I/O call
|
||||
public async Task<string> FetchDataAsync(Uri uri, CancellationToken ct = default)
|
||||
|
|
@ -531,13 +652,16 @@ public async Task<string> FetchDataAsync(Uri uri)
|
|||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: ValueTask when result is often available synchronously
|
||||
// CORRECT: ValueTask when result is often available synchronously and the call is on a hot path
|
||||
public ValueTask<int> GetCachedCountAsync()
|
||||
{
|
||||
if (_cache.TryGetValue("count", out int count))
|
||||
return ValueTask.FromResult(count);
|
||||
return new ValueTask<int>(LoadCountFromDbAsync());
|
||||
}
|
||||
|
||||
// Note: do not await the same ValueTask twice, do not call AsTask() multiple times,
|
||||
// and do not mix consumption techniques on the same instance — undefined behavior.
|
||||
```
|
||||
|
||||
```csharp
|
||||
|
|
@ -551,19 +675,15 @@ public async Task<int> GetCachedCountAsync()
|
|||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: async Task for async event handlers
|
||||
// CORRECT: async Task for async event handlers exposed as awaitable
|
||||
public async Task HandleEventAsync(GameEvent e, CancellationToken ct)
|
||||
{
|
||||
await notificationService.SendAsync(e.PlayerId, ct);
|
||||
}
|
||||
=> await notificationService.SendAsync(e.PlayerId, ct);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: async void — exceptions are unobservable, cannot be awaited
|
||||
public async void HandleEvent(GameEvent e)
|
||||
{
|
||||
await notificationService.SendAsync(e.PlayerId, default);
|
||||
}
|
||||
=> await notificationService.SendAsync(e.PlayerId, default);
|
||||
```
|
||||
|
||||
```csharp
|
||||
|
|
@ -572,15 +692,25 @@ var packet = await reader.ReadPacketAsync(ct);
|
|||
```
|
||||
|
||||
```csharp
|
||||
// WRONG: .Result / .Wait() causes deadlocks
|
||||
// WRONG: .Result / .Wait() / GetAwaiter().GetResult() — deadlocks and thread pool starvation
|
||||
var packet = reader.ReadPacketAsync(ct).Result;
|
||||
var packet2 = reader.ReadPacketAsync(ct).GetAwaiter().GetResult();
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: ConfigureAwait(false) in library code
|
||||
// CORRECT: ConfigureAwait(false) in general-purpose library code
|
||||
var data = await stream.ReadAsync(buffer, ct).ConfigureAwait(false);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: Task.WhenAll for independent concurrent I/O
|
||||
var userTask = repo.GetUserAsync(id, ct);
|
||||
var ordersTask = repo.GetOrdersAsync(id, ct);
|
||||
await Task.WhenAll(userTask, ordersTask);
|
||||
return new Dashboard(await userTask, await ordersTask);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// CORRECT: IAsyncEnumerable for streaming
|
||||
public async IAsyncEnumerable<ChatMessage> ReadChatStreamAsync(
|
||||
[EnumeratorCancellation] CancellationToken ct = default)
|
||||
|
|
@ -593,6 +723,53 @@ public async IAsyncEnumerable<ChatMessage> ReadChatStreamAsync(
|
|||
await using var conn = new McConnection(host, port);
|
||||
```
|
||||
|
||||
### Common traps
|
||||
|
||||
- Calling `.Result`, `.Wait()`, or `GetAwaiter().GetResult()` inside normal async-capable code
|
||||
- Recommending `ConfigureAwait(false)` everywhere because "it is .NET Core" or "it prevents deadlocks"
|
||||
- Recommending `Task.Run` inside ASP.NET Core request code just to make code "more async" — request code already runs on the thread pool
|
||||
- Recommending `ValueTask` for every hot-looking method without checking completion behavior, call frequency, or single-consumer assumptions
|
||||
- Ignoring cancellation after plumbing a `CancellationToken` (accepting it but never checking or propagating)
|
||||
- Using `Task.WhenAny` without awaiting the returned winner task or handling the remaining tasks
|
||||
- Treating fire-and-forget as harmless when it touches scoped services, `HttpContext`, or unobserved failures
|
||||
- Awaiting the same `ValueTask` twice, or mixing `AsTask()` with `await` on the same instance
|
||||
|
||||
### Rationalization traps
|
||||
|
||||
| Rationalization | Better reasoning |
|
||||
|---|---|
|
||||
| "It works, so `.Result` is fine." | Lack of failure under one context does not make blocking safe or scalable. |
|
||||
| "`ValueTask` is always faster." | It trades simplicity for niche allocation wins and stricter consumption rules. |
|
||||
| "`ConfigureAwait(false)` everywhere is modern guidance." | Library and app code have different constraints. Blanket rules are weak. |
|
||||
| "`Task.Run` makes server code asynchronous." | It only queues work; it does not turn blocking I/O into true async I/O. |
|
||||
| "Fire-and-forget is okay because logging exists." | Logging does not solve scope lifetime, shutdown, retries, or error propagation. |
|
||||
|
||||
### Hard boundaries
|
||||
|
||||
- Do not endorse sync-over-async as a normal design choice
|
||||
- Do not suggest `async void` except for event handlers
|
||||
- Do not suggest `ValueTask` unless single-consumer / hot-path / measurement constraints are met
|
||||
- Do not claim `ConfigureAwait(false)` is always needed or always unnecessary
|
||||
- Do not approve fire-and-forget unless ownership, exception handling, and lifetime are explicit
|
||||
|
||||
### Output contract for review and design
|
||||
|
||||
When you review or design async code, label your reasoning:
|
||||
|
||||
- **Fact** — official runtime or API behavior
|
||||
- **Expert guidance** — interpretation from strong experts (Stephen Toub, Stephen Cleary, Andrew Arnott) when it adds design meaning
|
||||
- **Synthesis** — your recommendation for this exact case
|
||||
|
||||
Do not present contextual advice as a universal law.
|
||||
|
||||
### References for deep async work
|
||||
|
||||
Load on demand:
|
||||
|
||||
- [references/async-core-guidance.md](references/async-core-guidance.md) — fact / expert / synthesis for `Task` vs `ValueTask`, blocking, cancellation, exception flow, `WhenAll` / `WhenAny`
|
||||
- [references/async-context-and-tradeoffs.md](references/async-context-and-tradeoffs.md) — library vs app, UI vs ASP.NET Core, `Task.Run` boundaries, fire-and-forget alternatives, `ConfigureAwait` strong vs weak recommendations, throttling
|
||||
- [references/async-source-notes.md](references/async-source-notes.md) — source attribution and authority breakdown
|
||||
|
||||
## LINQ
|
||||
|
||||
### Prefer Method Syntax for Most Operations
|
||||
|
|
@ -699,7 +876,11 @@ for (int i = 0; i < data.Length; i++)
|
|||
int found = data.ToArray().Count(b => b == target);
|
||||
```
|
||||
|
||||
## Performance (.NET 8+)
|
||||
## Performance (.NET 8+ — works on both targets)
|
||||
|
||||
All APIs below ship in .NET 8 and continued unchanged in .NET 10. For benchmarks and rationale see Stephen Toub's deep dives:
|
||||
[Performance Improvements in .NET 8](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/) ·
|
||||
[Performance Improvements in .NET 10](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-10/) (covers JIT array-interface devirtualisation that speeds up many LINQ paths in .NET 10).
|
||||
|
||||
### Span\<T\> / Memory\<T\>
|
||||
|
||||
|
|
@ -803,7 +984,7 @@ foreach (var s in items) combined += s + ", ";
|
|||
| Immutable snapshots | `ImmutableDictionary<K,V>` | Persistent structure |
|
||||
| Membership test | `HashSet<T>` / `FrozenSet<T>` | FrozenSet for static |
|
||||
| Priority queue | `PriorityQueue<E,P>` | .NET 6+ |
|
||||
| Synchronization | `System.Threading.Lock` | C# 13; prefer over `lock(obj)` |
|
||||
| Synchronization | `System.Threading.Lock` / `lock(object)` | `Lock` is .NET 9+ only — on .NET 8 use `private readonly object _gate = new();` |
|
||||
| Producer-consumer | `Channel<T>` | Over `BlockingCollection<T>` |
|
||||
| Temp buffer | `ArrayPool<T>` / `stackalloc` | Zero/low alloc |
|
||||
|
||||
|
|
@ -960,10 +1141,14 @@ public bool IsAlive => Health > 0;
|
|||
_ = int.TryParse(s, out int result);
|
||||
(_, int y, _) = GetCoordinates();
|
||||
|
||||
// CORRECT: nameof for resilient refactoring (unbound generics in C# 14)
|
||||
// CORRECT: nameof for resilient refactoring
|
||||
throw new ArgumentException("Invalid value", nameof(packetId));
|
||||
LogToConsole($"{nameof(AutoEat)}: eating {item.Name}");
|
||||
string typeName = nameof(Dictionary<,>); // "Dictionary"
|
||||
|
||||
// .NET 10 / C# 14 — unbound generic in nameof
|
||||
string typeName10 = nameof(Dictionary<,>); // "Dictionary"
|
||||
// .NET 8 / C# 12 — use any closed generic instead
|
||||
string typeName8 = nameof(Dictionary<object, object>); // "Dictionary"
|
||||
|
||||
// CORRECT: static lambdas prevent accidental closure allocations
|
||||
list.Sort(static (a, b) => a.Id.CompareTo(b.Id));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
---
|
||||
description: >-
|
||||
Context-specific async guidance for library code, ui apps, asp.net core,
|
||||
background work, task.run, configureawait, and performance-sensitive design.
|
||||
metadata:
|
||||
tags: [configureawait, task.run, asp.net core, ui, library, performance]
|
||||
source: mixed
|
||||
---
|
||||
|
||||
# Context and Tradeoffs
|
||||
|
||||
## Library code versus app code
|
||||
|
||||
### General-purpose library code
|
||||
- Prefer APIs that expose true async for I/O-bound work.
|
||||
- Do not add async wrappers around purely compute-bound methods just to look modern. Expose sync compute APIs and let callers decide whether to offload.
|
||||
- `ConfigureAwait(false)` is a strong default when the library does not need the caller’s context.
|
||||
- Avoid ambient assumptions about a UI thread, request context, or test framework behavior.
|
||||
|
||||
### App code
|
||||
- Prefer the style that fits the app model.
|
||||
- UI code often needs the original context after `await`.
|
||||
- ASP.NET Core request code normally does not need `Task.Run` just to stay responsive, because it already runs on thread pool threads.
|
||||
- Do not present “ASP.NET Core has no synchronization context” as proof that every `ConfigureAwait(false)` discussion is obsolete.
|
||||
|
||||
## `Task.Run` boundaries
|
||||
|
||||
### Good uses
|
||||
- Offload CPU-bound work so a UI thread can stay responsive.
|
||||
- Offload CPU work from a caller when that scheduling boundary is deliberate.
|
||||
|
||||
### Weak uses
|
||||
- Wrapping synchronous I/O to pretend it is true async I/O.
|
||||
- Calling `Task.Run` and immediately awaiting it in ASP.NET Core request handling when no CPU offload goal exists.
|
||||
- Using `Task.Run` to hide blocking APIs instead of fixing the underlying API choice.
|
||||
|
||||
## Fire-and-forget
|
||||
|
||||
### Assume unsafe until proven otherwise
|
||||
A background task needs answers for all of these:
|
||||
- Who owns its lifetime?
|
||||
- How are exceptions observed?
|
||||
- How does shutdown cancel it?
|
||||
- Does it touch scoped services or request-bound objects?
|
||||
- Does work need retries, backpressure, or queueing?
|
||||
|
||||
### Safer alternatives
|
||||
- Await the task normally.
|
||||
- Queue work to an owned background component.
|
||||
- In ASP.NET Core, prefer hosted services or a dedicated background queue pattern for long-lived work.
|
||||
- If scoped services are required in background processing, create an explicit scope instead of capturing request scope objects.
|
||||
|
||||
## `ConfigureAwait`
|
||||
|
||||
### Strong recommendation
|
||||
- In general-purpose libraries, use `ConfigureAwait(false)` unless the continuation must run in the captured context.
|
||||
|
||||
### Weak recommendation
|
||||
- “Always use it in app code.”
|
||||
- “Never use it on .NET Core.”
|
||||
- “Use it once at the first await and you are done.”
|
||||
|
||||
### Review note
|
||||
If code after the `await` needs a specific context, say so explicitly. If it does not, the recommendation depends on whether the code is app-level or general-purpose library code.
|
||||
|
||||
## Performance guidance
|
||||
|
||||
### Correctness first
|
||||
Do not trade API clarity for speculative micro-optimizations.
|
||||
|
||||
### `ValueTask` is performance-specialized
|
||||
Recommend it only when most of these are true:
|
||||
1. the method is called very frequently
|
||||
2. it often completes synchronously or from a reusable source
|
||||
3. allocation reduction matters on measurements
|
||||
4. consumers can respect single-consumer semantics
|
||||
5. task combinator ergonomics are not central to the API
|
||||
|
||||
### Throttling and concurrency control
|
||||
- `Task.WhenAll` expresses concurrency; it does not limit it.
|
||||
- For bounded concurrency, use an async gate such as `SemaphoreSlim.WaitAsync`, or platform helpers such as `Parallel.ForEachAsync` when the workload fits.
|
||||
- Always define what happens to remaining work after the first completion or first failure.
|
||||
105
.skills/csharp-best-practices/references/async-core-guidance.md
Normal file
105
.skills/csharp-best-practices/references/async-core-guidance.md
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
---
|
||||
description: >-
|
||||
Source-backed core guidance for task, valuetask, cancellation, exception flow,
|
||||
blocking, and concurrency in c# async code reviews and implementations.
|
||||
metadata:
|
||||
tags: [csharp, async, task, valuetask, cancellation, exceptions, concurrency]
|
||||
source: mixed
|
||||
---
|
||||
|
||||
# Core Guidance
|
||||
|
||||
## Facts from official .NET documentation
|
||||
|
||||
### 1. Return types and `async void`
|
||||
- Async methods should normally return `Task` or `Task<T>`.
|
||||
- `async void` is intended for event handlers; callers cannot await it and exception handling differs.
|
||||
- TAP methods that return awaitable types conventionally use the `Async` suffix.
|
||||
|
||||
### 2. Blocking on async
|
||||
- `Task<T>.Result` is blocking. Prefer `await` in most cases.
|
||||
- Blocking can deadlock in context-bound environments and reduces scalability even when it does not deadlock.
|
||||
- `await` on a faulted task rethrows one exception directly; `.Wait()` and `.Result` wrap failures in `AggregateException`.
|
||||
|
||||
### 3. `Task` versus `ValueTask`
|
||||
- Default to `Task` or `Task<T>` unless there is a demonstrated reason not to.
|
||||
- `ValueTask` has stricter usage rules. A given instance should generally be awaited only once.
|
||||
- Do not await the same `ValueTask` multiple times, call `AsTask()` multiple times, or mix consumption techniques on the same instance.
|
||||
- For synchronously successful `Task`-returning methods, `Task.CompletedTask` is the normal zero-result completion value.
|
||||
|
||||
### 4. Cancellation
|
||||
- If a TAP method supports cancellation, expose a `CancellationToken`.
|
||||
- Pass the token to nested operations that should participate in cancellation.
|
||||
- If an async method throws `OperationCanceledException` associated with the method’s token, the returned task transitions to `Canceled`.
|
||||
- After a method has completed its work successfully, do not report cancellation instead of success.
|
||||
|
||||
### 5. Exception flow and task combinators
|
||||
- `Task.WhenAll` does not block the calling thread.
|
||||
- If any supplied task faults, the `WhenAll` task faults and aggregates the unwrapped exceptions from the component tasks.
|
||||
- If none fault and at least one is canceled, the `WhenAll` task is canceled.
|
||||
- `Task.WhenAny` returns a task that completes successfully with the first completed task as its result, even when that winning task itself is faulted or canceled.
|
||||
- After `WhenAny`, await the returned winner task to propagate its outcome.
|
||||
- The remaining tasks continue unless you cancel or otherwise handle them.
|
||||
|
||||
## Expert guidance that is strong and technically grounded
|
||||
|
||||
### Stephen Toub
|
||||
- Use `ConfigureAwait(false)` as the general default for general-purpose library code, because library code should not depend on an app model’s context.
|
||||
- App-level code is different. UI code often needs the captured context. ASP.NET Core also changes the deadlock discussion because it does not install the classic ASP.NET style synchronization context, but that does not make blanket `ConfigureAwait` advice strong.
|
||||
- `ValueTask<T>` exists mainly to avoid allocations on frequently synchronous success paths. It is not a general replacement for `Task<T>` because `Task` is more flexible for multiple awaits, caching, and combinators.
|
||||
|
||||
### Andrew Arnott
|
||||
- Propagate the token until the point of no cancellation.
|
||||
- Validate arguments before cancellation checks when argument validation should always run.
|
||||
- Prefer catching `OperationCanceledException` rather than `TaskCanceledException` in general-purpose logic.
|
||||
- Keep `CancellationToken` last in the parameter list; make it optional mainly on public APIs, not necessarily on internal methods.
|
||||
|
||||
### Stephen Cleary
|
||||
- “Async all the way” is a strong design guideline, not an absolute law of physics. Sync bridges exist, but they are specialized boundary decisions, not a normal code review recommendation.
|
||||
- `async void` and sync-over-async both create real observability and composition problems even when a sample appears to work.
|
||||
|
||||
## Naming and testability
|
||||
|
||||
### Naming
|
||||
- TAP methods that return awaitable types conventionally use the `Async` suffix. Do not force renames when an interface, base class, or event pattern already dictates the name.
|
||||
|
||||
### Testability
|
||||
- Favor awaitable APIs over hidden work so tests can await completion, assert faults, and drive cancellation deterministically.
|
||||
- Prefer explicit background components, injected clocks, and owned queues over ad hoc fire-and-forget logic that tests cannot observe.
|
||||
|
||||
## Synthesis for agents
|
||||
|
||||
### Code review defaults
|
||||
- Treat `.Result`, `.Wait()`, and `GetAwaiter().GetResult()` as likely defects unless the code is a deliberate sync boundary and the caller explicitly cannot be async.
|
||||
- Prefer `Task`/`Task<T>` for API design. Require an explicit reason before recommending `ValueTask`.
|
||||
- Require cancellation behavior to be coherent: accepted, propagated, and not silently dropped.
|
||||
- Prefer `await Task.WhenAll(...)` for independent operations started before awaiting.
|
||||
- Treat `Task.WhenAny(...)` as incomplete until the winner is awaited and losers are canceled, observed, or intentionally left running.
|
||||
|
||||
### Minimal examples
|
||||
|
||||
#### Avoid sync-over-async
|
||||
```csharp
|
||||
// bad
|
||||
var user = client.GetUserAsync(id).Result;
|
||||
|
||||
// better
|
||||
var user = await client.GetUserAsync(id);
|
||||
```
|
||||
|
||||
#### Use `Task.WhenAll` for parallel I/O
|
||||
```csharp
|
||||
var userTask = repo.GetUserAsync(id, ct);
|
||||
var ordersTask = repo.GetOrdersAsync(id, ct);
|
||||
await Task.WhenAll(userTask, ordersTask);
|
||||
return new Dashboard(await userTask, await ordersTask);
|
||||
```
|
||||
|
||||
#### Be conservative with `ValueTask`
|
||||
```csharp
|
||||
// default
|
||||
Task<Item?> GetAsync(string key, CancellationToken ct);
|
||||
|
||||
// specialized hot path only when justified
|
||||
ValueTask<Item?> TryGetCachedAsync(string key);
|
||||
```
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
description: >-
|
||||
Authority notes and citations for the c# async best practices skill, separating
|
||||
official documentation, expert interpretation, and synthesized guidance.
|
||||
metadata:
|
||||
tags: [sources, citations, authority, notes]
|
||||
source: external
|
||||
---
|
||||
|
||||
# Source Notes
|
||||
|
||||
## Official facts
|
||||
|
||||
- Microsoft Learn, "Implementing the Task-based Asynchronous Pattern"
|
||||
- https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/implementing-the-task-based-asynchronous-pattern
|
||||
- Return types, cancellation behavior, `Task.Run` boundaries, and TAP implementation guidance.
|
||||
- Microsoft Learn, "Consuming the Task-based Asynchronous Pattern"
|
||||
- https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern
|
||||
- `await`, `WhenAll`, `WhenAny`, cancellation propagation, and exception behavior.
|
||||
- Microsoft Learn, "Async return types"
|
||||
- https://learn.microsoft.com/en-us/dotnet/csharp/asynchronous-programming/async-return-types
|
||||
- `Task`, `Task<T>`, `async void`, generalized async return types.
|
||||
- Microsoft Learn, `ValueTask` API reference
|
||||
- https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.valuetask
|
||||
- single-consumer warnings and default-to-`Task` guidance.
|
||||
- Microsoft Learn, ASP.NET Core best practices
|
||||
- https://learn.microsoft.com/en-us/aspnet/core/fundamentals/best-practices
|
||||
- avoid blocking calls, avoid unnecessary `Task.Run`, background-work cautions.
|
||||
- Microsoft Learn, hosted services in ASP.NET Core
|
||||
- https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services
|
||||
- safe long-lived background work and cancellation during shutdown.
|
||||
|
||||
## Expert guidance used only when technically grounded
|
||||
|
||||
- Stephen Toub, ".NET Blog: ConfigureAwait FAQ"
|
||||
- https://devblogs.microsoft.com/dotnet/configureawait-faq/
|
||||
- best source for context capture semantics and library-vs-app guidance.
|
||||
- Stephen Toub, ".NET Blog: Understanding the Whys, Whats, and Whens of ValueTask"
|
||||
- https://devblogs.microsoft.com/dotnet/understanding-the-whys-whats-and-whens-of-valuetask/
|
||||
- performance rationale and tradeoffs behind `ValueTask<T>`.
|
||||
- Stephen Toub, ".NET Blog: Await, and UI, and deadlocks! Oh my!"
|
||||
- https://devblogs.microsoft.com/dotnet/await-and-ui-and-deadlocks-oh-my/
|
||||
- canonical deadlock explanation for context-bound code.
|
||||
- Stephen Toub, ".NET Blog: Task Exception Handling in .NET 4.5"
|
||||
- https://devblogs.microsoft.com/dotnet/task-exception-handling-in-net-4-5/
|
||||
- explains `await` versus blocking exception shape and why `WhenAll` matters.
|
||||
- Andrew Arnott, "Recommended patterns for CancellationToken"
|
||||
- https://devblogs.microsoft.com/premier-developer/recommended-patterns-for-cancellationtoken/
|
||||
- practical cancellation design heuristics; useful, but not treated as a language/runtime spec.
|
||||
- Stephen Cleary, "Async/Await - Best Practices in Asynchronous Programming"
|
||||
- https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming
|
||||
- useful design interpretation, but older and treated as contextual guidance rather than current official policy.
|
||||
|
||||
## Where the skill is intentionally cautious
|
||||
|
||||
- `ConfigureAwait`: strong guidance exists for libraries, weaker guidance for app code. Blanket rules are rejected.
|
||||
- `Task.Run`: valid for deliberate CPU offload, weak as a server-side patch for blocking I/O.
|
||||
- `ValueTask`: supported and useful, but easy to misuse. The skill defaults to `Task` unless evidence is present.
|
||||
- Fire-and-forget: acceptable only with explicit ownership and lifecycle design, especially in server code.
|
||||
|
|
@ -1,166 +0,0 @@
|
|||
---
|
||||
name: csharp-dotnet-cli-optimization
|
||||
description: >-
|
||||
Use when diagnosing or optimizing generic C#/.NET performance, GC pressure,
|
||||
allocations, heap or stack usage, LINQ overhead, boxing, Span/Memory,
|
||||
stackalloc, pooling, or hot-path code with CLI-first tools such as
|
||||
dotnet-counters, dotnet-trace, dotnet-stack, dotnet-gcdump, dotnet-dump, or
|
||||
BenchmarkDotNet.
|
||||
metadata:
|
||||
category: technique
|
||||
triggers:
|
||||
- dotnet-counters
|
||||
- dotnet-trace
|
||||
- dotnet-dump
|
||||
- dotnet-gcdump
|
||||
- dotnet-stack
|
||||
- benchmarkdotnet
|
||||
- allocations
|
||||
- gc pressure
|
||||
- memory leak
|
||||
- hot path
|
||||
- linq
|
||||
- stackalloc
|
||||
- span
|
||||
- memory
|
||||
- boxing
|
||||
- heap
|
||||
- stack
|
||||
- latency
|
||||
- throughput
|
||||
- slow
|
||||
- hang
|
||||
- deadlock
|
||||
---
|
||||
|
||||
# C#/.NET CLI Optimization
|
||||
|
||||
CLI-first guidance for generic C# 14 / .NET 10 performance work.
|
||||
Use the references on demand:
|
||||
|
||||
- Read [references/memory-model-gc.md](references/memory-model-gc.md) for stack vs heap, generations, LOH, pinning, server vs workstation GC, and GC tuning limits.
|
||||
- Read [references/code-patterns.md](references/code-patterns.md) for LINQ, Span/Memory, stackalloc, structs, boxing, pooling, strings, and analyzer-backed code patterns.
|
||||
- Read [references/README.md](references/README.md) for dated sources and freshness notes.
|
||||
|
||||
## When to Use
|
||||
|
||||
- A .NET process is slow, allocation-heavy, CPU-heavy, or memory-hungry
|
||||
- A live process appears stuck, hung, or deadlocked
|
||||
- The user asks how heap, stack, GC, boxing, or LINQ overhead actually works in .NET
|
||||
- The user wants concrete bad vs good code patterns after measurement has identified a hot path
|
||||
- The task needs a decision between counters, traces, stacks, GC dumps, dumps, or a benchmark
|
||||
|
||||
**NOT for:**
|
||||
- ASP.NET Core, EF Core, MAUI, Orleans, Unity, Avalonia, WPF, WinForms, Blazor, or other framework-specific playbooks
|
||||
- Visual Studio, Rider, VS Code, PerfView, speedscope, or any GUI-first workflow
|
||||
- speculative rewrites such as "replace everything with Span" before measurement
|
||||
|
||||
## Iron Rule
|
||||
|
||||
ALWAYS measure first, change second, and re-measure third.
|
||||
|
||||
NEVER claim an optimization without before/after evidence from the same scenario.
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "This is obviously slow" | The runtime, JIT, and libraries often invalidate intuition. |
|
||||
| "struct means stack" | Value types are stored inline. They are not "always on the stack". |
|
||||
| "All LINQ is slow" | .NET 10 improved many LINQ paths. Measure before rewriting. |
|
||||
| "GC.Collect will fix it" | Forced collection usually treats symptoms, not cause. |
|
||||
|
||||
## Investigation Order
|
||||
|
||||
1. Use `dotnet-counters` for live triage.
|
||||
2. If the process is stuck, capture `dotnet-stack` immediately.
|
||||
3. If CPU or allocation hot paths matter, collect `dotnet-trace`.
|
||||
4. If heap growth matters more than call paths, collect `dotnet-gcdump`.
|
||||
5. If you need SOS heap inspection or a postmortem, collect `dotnet-dump`.
|
||||
6. Only after live evidence points to a candidate routine, apply patterns from the reference docs.
|
||||
7. If the change is truly local and isolated, use BenchmarkDotNet to compare implementations.
|
||||
8. Re-run the original live capture to prove the real workload improved.
|
||||
|
||||
## Which Reference to Load
|
||||
|
||||
| User question | Read first |
|
||||
|---|---|
|
||||
| "How do stack and heap really work in .NET?" | `references/memory-model-gc.md` |
|
||||
| "Why is GC pausing or why is LOH churn hurting us?" | `references/memory-model-gc.md` |
|
||||
| "How should I optimize this LINQ?" | `references/code-patterns.md` |
|
||||
| "Can I move this to the stack with stackalloc or Span?" | `references/code-patterns.md` |
|
||||
| "Should this be a struct, ref struct, readonly struct, or class?" | `references/code-patterns.md` and `references/memory-model-gc.md` |
|
||||
| "Why is this boxing?" | `references/code-patterns.md` |
|
||||
|
||||
## Tool Selection
|
||||
|
||||
| Question | Tool | What it answers | Typical next step |
|
||||
|---|---|---|---|
|
||||
| Is the live process allocating, GCing, or saturating CPU? | `dotnet-counters` | Live counters and trend direction | Capture a trace or GC dump if suspicious |
|
||||
| Is the process hung or deadlocked right now? | `dotnet-stack` | Current managed stack snapshot | Collect a dump if you need deeper postmortem evidence |
|
||||
| Which call paths consume CPU or allocate heavily? | `dotnet-trace` | Sampled execution and runtime events | Confirm hot paths, then isolate code |
|
||||
| Which object types dominate managed heap usage? | `dotnet-gcdump` | Heap composition and type totals | Decide whether to redesign lifetimes or collect a full dump |
|
||||
| Do I need SOS heap inspection or thread state? | `dotnet-dump` | Full dump plus CLI analysis | Run `analyze -c` commands |
|
||||
| Did a code change improve one isolated routine? | BenchmarkDotNet | Reproducible microbenchmark comparison | Re-run live diagnostics in the real scenario |
|
||||
|
||||
## Pattern Guardrails
|
||||
|
||||
- Do not answer "put it on the stack" as a blanket goal. Explain lifetime, copies, boxing, and escape rules instead.
|
||||
- Do not suggest `stackalloc` for unbounded sizes, large buffers, or loop-carried allocations.
|
||||
- Do not recommend `Span<T>` for data that must cross `await`, escape to the heap, or live in object fields. Switch to `Memory<T>` or `ReadOnlyMemory<T>` for that.
|
||||
- Do not recommend converting every `class` to a `struct`. Large, mutable, identity-bearing, or frequently boxed types often get worse.
|
||||
- Do not blanket-rewrite LINQ to loops. Use analyzer-backed fixes first, and remember .NET 10 substantially improved many LINQ paths.
|
||||
- Do not recommend pooling without ownership rules. Returned pooled arrays must not be reused by the caller.
|
||||
- Do not recommend `GC.Collect()` except for rare, justified lifecycle boundaries, and only with measurement.
|
||||
|
||||
## Analyzer Radar
|
||||
|
||||
When performance diagnostics point to code patterns rather than runtime configuration, consult the current performance analyzers, especially:
|
||||
|
||||
- `CA1826`, `CA1827`, `CA1829`, `CA1836`, `CA1851`, `CA1860` for LINQ and enumeration
|
||||
- `CA1845`, `CA1846`, `CA1858` for string and span-friendly APIs
|
||||
- `CA1834`, `CA1865-CA1867` for `StringBuilder` char overloads
|
||||
- `CA1870` for cached `SearchValues<T>`
|
||||
|
||||
These rules are clues, not goals. Apply them where the measured hot path justifies it.
|
||||
|
||||
## Minimal Commands
|
||||
|
||||
```bash
|
||||
dnx dotnet-counters monitor --process-id <PID>
|
||||
dotnet-counters monitor -p <PID> --counters System.Runtime
|
||||
dotnet-stack report -p <PID>
|
||||
dotnet-trace collect -p <PID> --duration 00:00:30
|
||||
dotnet-trace report <trace.nettrace> topN
|
||||
dotnet-gcdump collect -p <PID>
|
||||
dotnet-gcdump report <file.gcdump>
|
||||
dotnet-dump collect -p <PID> --type Heap
|
||||
dotnet-dump analyze <dump> -c "dumpheap -stat" -c "exit"
|
||||
```
|
||||
|
||||
Minimal BenchmarkDotNet pattern:
|
||||
|
||||
```csharp
|
||||
using BenchmarkDotNet.Attributes;
|
||||
|
||||
[MemoryDiagnoser]
|
||||
public class CandidateBench
|
||||
{
|
||||
[Benchmark(Baseline = true)]
|
||||
public int Original() => OriginalImpl();
|
||||
|
||||
[Benchmark]
|
||||
public int Candidate() => CandidateImpl();
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
dotnet run -c Release
|
||||
```
|
||||
|
||||
## Output
|
||||
|
||||
When using this skill, report:
|
||||
|
||||
- the measured symptom and the evidence used to identify it
|
||||
- the chosen tool or code pattern and why it fits this bottleneck
|
||||
- the relevant tradeoff, such as allocation vs copy cost, deferred vs eager execution, or stack vs pool
|
||||
- the before/after result, or say explicitly if the recommendation is still unverified
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
---
|
||||
description: Dated source ledger for csharp-dotnet-cli-optimization.
|
||||
metadata:
|
||||
tags: [sources, diagnostics, gc, linq, span, stackalloc, boxing]
|
||||
---
|
||||
|
||||
# Sources
|
||||
|
||||
## Current primary sources
|
||||
|
||||
- [dotnet-counters](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters), Microsoft Learn, updated `2025-10-02`
|
||||
- Canonical CLI docs for live counters, `monitor`, `collect`, and `dnx` one-shot execution on .NET 10.0.100+.
|
||||
- [dotnet-trace](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-trace), Microsoft Learn, updated `2026-03-20`
|
||||
- Canonical CLI docs for `collect`, `report`, and the preview `collect-linux` path plus its limits.
|
||||
- [dotnet-dump](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-dump), Microsoft Learn, updated `2026-03-04`
|
||||
- Canonical CLI docs for dump collection, dump types, and `analyze -c`.
|
||||
- [dotnet-gcdump](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-gcdump), Microsoft Learn, updated `2025-12-17`
|
||||
- Canonical CLI docs for GC dump collection, `report`, and the induced full Gen 2 GC caveat.
|
||||
- [Fundamentals of garbage collection](https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/fundamentals), Microsoft Learn, updated `2025-10-22`
|
||||
- Current official overview of generations, allocation, and managed heap behavior.
|
||||
- [Runtime configuration options for garbage collection](https://learn.microsoft.com/en-us/dotnet/core/runtime-config/garbage-collector), Microsoft Learn, updated `2025-11-22`
|
||||
- Current official source for server vs workstation GC, background GC, heap limits, LOH threshold, and modern GC configuration behavior.
|
||||
- [stackalloc expression](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/stackalloc), Microsoft Learn, updated `2026-01-24`
|
||||
- Current official guidance for stack allocation limits, loop avoidance, initialization, and Span-based usage.
|
||||
- [ref struct types](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/ref-struct), Microsoft Learn, updated `2026-01-20`
|
||||
- Current official guidance for stack-only semantics and escape restrictions.
|
||||
- [Structure types](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/struct), Microsoft Learn, updated `2026-01-14`
|
||||
- Current official source for readonly structs, pass-by-reference guidance, and boxing conversions.
|
||||
- [Value types](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/value-types), Microsoft Learn, updated `2026-01-20`
|
||||
- Current official source for copy semantics and inline storage behavior.
|
||||
- [Boxing and Unboxing](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/types/boxing-and-unboxing), Microsoft Learn, updated `2025-10-13`
|
||||
- Current official source for boxing semantics and cost.
|
||||
- [Memory<T> and Span<T> usage guidelines](https://learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines), Microsoft Learn, updated `2025-04-11`
|
||||
- Current official guidance for choosing `Span<T>` vs `Memory<T>` and ownership rules.
|
||||
- [Lambda expressions](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions), Microsoft Learn, updated `2026-01-24`
|
||||
- Current official source for capture semantics and `static` lambdas.
|
||||
- [What's new in C# 14](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-14), Microsoft Learn, updated `2025-11-19`
|
||||
- Current official confirmation of first-class span conversions in C# 14.
|
||||
- [Performance rules](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/performance-warnings), Microsoft Learn, updated `2025-10-29`
|
||||
- Current official index of analyzer-backed performance rules, including `CA1870`.
|
||||
- [Performance Improvements in .NET 10](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-10/), Stephen Toub, published `2025-09-10`
|
||||
- High-trust expert source showing real .NET 10 runtime and LINQ improvements. Use it to avoid stale folklore such as "all LINQ is slow".
|
||||
|
||||
## Specific analyzer pages used for code-pattern guidance
|
||||
|
||||
- [CA1827](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1827), updated `2023-11-14`
|
||||
- [CA1845](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1845), updated `2024-11-12`
|
||||
- [CA1846](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1846), updated `2023-12-16`
|
||||
- [CA1851](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1851), updated `2023-11-14`
|
||||
- [CA1858](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1858), current official analyzer page
|
||||
- [CA1860](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1860), current official analyzer page
|
||||
|
||||
## Older but still canonical sources used cautiously
|
||||
|
||||
- [Reduce memory allocations using new C# features](https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/performance/), Microsoft Learn, updated `2023-10-17`
|
||||
- Still useful for `ref`, `in`, readonly struct, and copy-avoidance guidance, but older than the core 2025-2026 docs.
|
||||
- [Intermediate materialization](https://learn.microsoft.com/en-us/dotnet/standard/linq/intermediate-materialization), Microsoft Learn, updated `2022-09-02`
|
||||
- Still canonical for LINQ materialization semantics.
|
||||
- [Deferred execution and lazy evaluation](https://learn.microsoft.com/en-us/dotnet/standard/linq/deferred-execution-lazy-evaluation), Microsoft Learn, updated `2022-09-29`
|
||||
- Still canonical for LINQ deferred-execution semantics.
|
||||
- [dotnet-stack](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-stack), Microsoft Learn, updated `2023-03-14`
|
||||
- Used only for narrow stack-snapshot guidance because the page is stale compared to the other diagnostics docs.
|
||||
|
||||
## Explicit exclusions
|
||||
|
||||
- Framework-specific tutorials were intentionally excluded.
|
||||
- GUI-first analysis flows were intentionally excluded.
|
||||
- MCC-specific paths, code, and hot-path examples were intentionally excluded.
|
||||
|
|
@ -1,267 +0,0 @@
|
|||
---
|
||||
name: csharp-optimization
|
||||
description: >-
|
||||
Use when optimizing C# code in MCC, reducing GC pressure, profiling hot paths,
|
||||
fixing latency spikes, or reviewing code for allocation or throughput issues.
|
||||
metadata:
|
||||
category: technique
|
||||
triggers: performance, allocations, GC, hot path, latency, throughput,
|
||||
memory pressure, optimize, slow, freeze, lag spike, packet processing speed
|
||||
---
|
||||
|
||||
# C# Performance Optimization for MCC
|
||||
|
||||
Hands-on optimization recipes for Minecraft Console Client hot paths.
|
||||
Complements `csharp-best-practices` (conventions) with measurement-driven
|
||||
performance work.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Profiling or reducing GC pressure in a running MCC session
|
||||
- Optimizing per-packet code (`Protocol18.HandlePacket`, `DataTypes.ReadNext*`)
|
||||
- Optimizing per-tick code (`PlayerPhysics.Tick`, `CollisionDetector.Collide`)
|
||||
- Speeding up chunk decoding (`Protocol18Terrain.ProcessChunkColumnData`)
|
||||
- Improving A* pathfinding (`Movement.CalculatePath`)
|
||||
- Reviewing any code change for allocation or throughput regressions
|
||||
|
||||
**NOT for:**
|
||||
- Login, config parsing, or one-shot command handlers (prefer clarity there)
|
||||
- Style/convention questions (use `csharp-best-practices` instead)
|
||||
|
||||
---
|
||||
|
||||
## Iron Rule: Measure First
|
||||
|
||||
**NEVER optimize without profiling data.**
|
||||
|
||||
Guessing which code is slow is wrong more often than right. Measure, change,
|
||||
re-measure. If you cannot show a before/after number, the optimization is not
|
||||
justified.
|
||||
|
||||
| Rationalization | Reality |
|
||||
|-----------------|---------|
|
||||
| "This is obviously slow" | Obvious to you is not obvious to the JIT. Measure. |
|
||||
| "I'll profile later" | Later never comes. Profile now or don't optimize. |
|
||||
| "It's just one allocation" | On a 20 TPS tick, one allocation = 20 per second = GC pressure. Measure. |
|
||||
| "AggressiveInlining everywhere" | The JIT already inlines small methods. Prove it helps before adding. |
|
||||
|
||||
---
|
||||
|
||||
## MCC Hot-Path Map
|
||||
|
||||
Know which code runs at which frequency before deciding where to invest:
|
||||
|
||||
| Frequency | Key paths (actual files) | Priority |
|
||||
|---|---|---|
|
||||
| Per-packet (100s/sec) | `Protocol/Handlers/Protocol18.cs` HandlePacket, `Protocol/Handlers/DataTypes.cs` ReadNext* | **High** |
|
||||
| Per-tick (20/sec) | `Physics/PlayerPhysics.cs` Tick, `Physics/CollisionDetector.cs` Collide, ChatBot `Update()` | **High** |
|
||||
| Per-chunk-load | `Protocol/Handlers/Protocol18Terrain.cs` ProcessChunkColumnData, ReadBlockStatesField | Medium |
|
||||
| Per-pathfind | `Mapping/Movement.cs` CalculatePath (A*) | Medium |
|
||||
| Per-connection | Login, registry sync, config | Low |
|
||||
| Per-user-action | Commands, chat | Low |
|
||||
|
||||
---
|
||||
|
||||
## Profiling Recipes
|
||||
|
||||
### 1. Live GC monitoring
|
||||
|
||||
```bash
|
||||
dotnet-counters ps # find MinecraftClient PID
|
||||
dotnet-counters monitor --process-id <PID> \
|
||||
--counters System.Runtime[gen-0-gc-count,gen-1-gc-count,gen-2-gc-count,alloc-rate]
|
||||
```
|
||||
|
||||
Healthy idle MCC: near-zero Gen-1/Gen-2 collections. Frequent Gen-0 during idle
|
||||
means a hot-path allocation needs attention.
|
||||
|
||||
### 2. Allocation tracking
|
||||
|
||||
```bash
|
||||
dotnet-trace collect --process-id <PID> \
|
||||
--providers Microsoft-Windows-DotNETRuntime:0x1:5
|
||||
```
|
||||
|
||||
Open `.nettrace` in PerfView to find top-allocated types and call stacks.
|
||||
|
||||
### 3. Isolated benchmarks (BenchmarkDotNet)
|
||||
|
||||
Extract the hot method, add `[MemoryDiagnoser]`. Key columns: **Mean**,
|
||||
**Allocated**, **Gen0**.
|
||||
|
||||
---
|
||||
|
||||
## Allocation Reduction (Highest Impact)
|
||||
|
||||
Reducing GC pressure directly reduces latency spikes in a long-running client.
|
||||
|
||||
### Pattern: Reuse per-tick buffers
|
||||
|
||||
```csharp
|
||||
// BEFORE: new List every tick (20 allocations/sec)
|
||||
var result = new List<Aabb>();
|
||||
|
||||
// AFTER: thread-local reuse (0 allocations/sec)
|
||||
[ThreadStatic] private static List<Aabb>? t_buf;
|
||||
var result = t_buf ??= new List<Aabb>(64);
|
||||
result.Clear();
|
||||
```
|
||||
|
||||
`[ThreadStatic]` works when single-threaded and non-reentrant (physics tick).
|
||||
If reentrant: use `ObjectPool<T>`. If cross-thread: use `ArrayPool<T>`.
|
||||
|
||||
### Pattern: stackalloc for small fixed buffers
|
||||
|
||||
MCC already does this in `DataTypes.cs` for endian-swapped reads:
|
||||
|
||||
```csharp
|
||||
Span<byte> rawValue = stackalloc byte[8];
|
||||
for (int i = 7; i >= 0; --i) rawValue[i] = cache.Dequeue();
|
||||
return BitConverter.ToDouble(rawValue);
|
||||
```
|
||||
|
||||
Rules: under 512 bytes, known size at compile time, never inside loops or recursion.
|
||||
|
||||
### Pattern: Span slicing instead of array copies
|
||||
|
||||
```csharp
|
||||
// BEFORE: allocates
|
||||
byte[] sub = new byte[length];
|
||||
Array.Copy(source, offset, sub, 0, length);
|
||||
|
||||
// AFTER: zero-copy
|
||||
ReadOnlySpan<byte> sub = source.AsSpan(offset, length);
|
||||
```
|
||||
|
||||
Critical in packet parsing where many fields are sliced from one buffer.
|
||||
|
||||
---
|
||||
|
||||
## Hot-Path Tuning
|
||||
|
||||
### MethodImpl attributes
|
||||
|
||||
MCC uses `[MethodImpl]` on its hottest paths. Match the attribute to the method:
|
||||
|
||||
| Attribute | When | MCC examples |
|
||||
|---|---|---|
|
||||
| `AggressiveInlining` | Tiny methods (< ~32 bytes IL), called millions of times | `Vec3d.Add`, `Aabb.Intersects`, `Chunk.SetWithoutCheck` |
|
||||
| `AggressiveOptimization` | Larger critical-path methods | `ReadBlockStatesField`, `ProcessChunkColumnData` |
|
||||
| Both | Medium methods, very high frequency | `DataTypes.ReadNextVarInt`, `ReadDataReverse` |
|
||||
| Neither | Infrequent code | Login, config, commands |
|
||||
|
||||
**Do not scatter `AggressiveInlining` without profiling evidence.** The JIT
|
||||
already inlines small methods.
|
||||
|
||||
### BinaryPrimitives over BitConverter
|
||||
|
||||
```csharp
|
||||
// BEFORE: manual endian swap
|
||||
(buf[0], buf[3]) = (buf[3], buf[0]);
|
||||
int val = BitConverter.ToInt32(buf);
|
||||
|
||||
// AFTER: direct big-endian read, no branch
|
||||
int val = BinaryPrimitives.ReadInt32BigEndian(buf);
|
||||
```
|
||||
|
||||
### MemoryMarshal for bulk reads
|
||||
|
||||
Already used in chunk decoding for zero-copy packed-long reads:
|
||||
```csharp
|
||||
ReadOnlySpan<long> longs = MemoryMarshal.Cast<byte, long>(entryData);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Structure Selection
|
||||
|
||||
### Frozen collections for palettes
|
||||
|
||||
Palette maps are built once and read millions of times. `FrozenDictionary`
|
||||
gives ~50% faster reads than `Dictionary`:
|
||||
|
||||
```csharp
|
||||
private static readonly FrozenDictionary<int, Material> s_palette =
|
||||
new Dictionary<int, Material> { ... }.ToFrozenDictionary();
|
||||
```
|
||||
|
||||
Apply to: `BlockPalettes/*.cs`, `EntityPalettes/*.cs`, `ItemPalettes/*.cs`,
|
||||
`PacketPalettes/*.cs`, any `static readonly Dictionary` populated once.
|
||||
|
||||
### PriorityQueue for A*
|
||||
|
||||
`Movement.cs` has a custom `BinaryHeap`. The built-in `PriorityQueue<TElement,
|
||||
TPriority>` (.NET 6+) is well-optimized and avoids maintenance burden.
|
||||
|
||||
### ConcurrentDictionary sizing
|
||||
|
||||
Pre-size `World.chunks` to avoid rehashing:
|
||||
```csharp
|
||||
new ConcurrentDictionary<(int, int), ChunkColumn>(
|
||||
concurrencyLevel: Environment.ProcessorCount, capacity: 1024);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Threading
|
||||
|
||||
### Minimize lock scope
|
||||
|
||||
Copy data out under the lock, process outside:
|
||||
```csharp
|
||||
List<Item> snapshot;
|
||||
lock (_lock) { snapshot = [.. _items]; }
|
||||
foreach (var item in snapshot) ExpensiveProcess(item);
|
||||
```
|
||||
|
||||
### Batch InvokeOnMainThread
|
||||
|
||||
Each `InvokeOnMainThread()` call blocks until the main thread runs it.
|
||||
In loops, batch into a single call:
|
||||
```csharp
|
||||
handler.InvokeOnMainThread(() =>
|
||||
{
|
||||
foreach (var entity in entities) UpdateEntity(entity);
|
||||
});
|
||||
```
|
||||
|
||||
### Channel\<T\> over BlockingCollection\<T\>
|
||||
|
||||
Lower overhead, async-friendly:
|
||||
```csharp
|
||||
var ch = Channel.CreateUnbounded<(int Id, Memory<byte> Data)>(
|
||||
new UnboundedChannelOptions { SingleReader = true });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Optimization Anti-Patterns
|
||||
|
||||
These are things agents (and humans) rationalize doing. Every one of them
|
||||
makes performance worse or wastes effort.
|
||||
|
||||
| Anti-pattern | Why it's wrong |
|
||||
|---|---|
|
||||
| Adding `AggressiveInlining` to large methods | Bloats call sites, causes more cache misses, makes code *slower* |
|
||||
| Optimizing login/config code | Runs once per session; clarity matters more than speed |
|
||||
| Using `ConcurrentDictionary` where a plain `Dictionary` + lock suffices | Concurrent overhead on uncontested paths costs more than a lock |
|
||||
| Replacing LINQ with manual loops on cold paths | No measurable gain, worse readability |
|
||||
| Caching mutable state to avoid re-reads | Stale cache bugs are harder to diagnose than the perf hit |
|
||||
| `Task.Result` / `.Wait()` on hot paths | Deadlock risk and thread-pool starvation |
|
||||
|
||||
---
|
||||
|
||||
## Pre-Commit Checklist
|
||||
|
||||
ALWAYS verify before submitting a performance change:
|
||||
|
||||
- [ ] Hot path identified with profiling data, not guesswork
|
||||
- [ ] Before/after measurements recorded (allocation count, throughput, or latency)
|
||||
- [ ] No new allocations inside per-tick or per-packet methods
|
||||
- [ ] `[MethodImpl]` attributes match method call frequency and IL size
|
||||
- [ ] Frozen collections used for any static lookup table
|
||||
- [ ] Lock scopes contain no I/O or expensive work
|
||||
- [ ] No `Task.Result`, `.Wait()`, or `GetAwaiter().GetResult()` on hot paths
|
||||
- [ ] Thread safety preserved (checked existing lock/concurrent patterns)
|
||||
- [ ] Optimization comments explain non-obvious choices
|
||||
- [ ] Code still compiles and passes all existing checks
|
||||
342
.skills/dotnet-performance-profiling-and-optimization/SKILL.md
Normal file
342
.skills/dotnet-performance-profiling-and-optimization/SKILL.md
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
---
|
||||
name: dotnet-performance-profiling-and-optimization
|
||||
description: >-
|
||||
Use when a .NET process is slow, hung, memory-heavy, or deadlocked, or when
|
||||
analyzing C#/ASP.NET Core code for performance anti-patterns across memory,
|
||||
async, LINQ, database, JSON, caching, DI, concurrency, HttpClient, exceptions,
|
||||
response, strings, startup, and metrics.
|
||||
metadata:
|
||||
category: technique
|
||||
triggers:
|
||||
- dotnet-counters
|
||||
- dotnet-trace
|
||||
- dotnet-dump
|
||||
- dotnet-gcdump
|
||||
- dotnet-stack
|
||||
- benchmarkdotnet
|
||||
- allocations
|
||||
- gc pressure
|
||||
- memory leak
|
||||
- hot path
|
||||
- linq
|
||||
- stackalloc
|
||||
- span
|
||||
- boxing
|
||||
- heap
|
||||
- latency
|
||||
- throughput
|
||||
- slow
|
||||
- hang
|
||||
- deadlock
|
||||
- optimize
|
||||
- performance
|
||||
- async
|
||||
- caching
|
||||
- di lifetime
|
||||
- ef core
|
||||
- cosmosdb
|
||||
- json serialization
|
||||
- httpclient
|
||||
- middleware
|
||||
version: 1.1.0
|
||||
platform: ".NET 8 and .NET 10 (no .NET 9 projects in scope)"
|
||||
---
|
||||
|
||||
# .NET Performance: Diagnostic & Code Review
|
||||
|
||||
Unified C#/.NET performance skill targeting **.NET 8 and .NET 10**. Two modes: live process diagnostics (Mode A) and static code optimization review with fixes (Mode B).
|
||||
|
||||
## Step 0 — Detect the target framework
|
||||
|
||||
Before recommending APIs, follow `../../references/detect-target-framework.md`. Many .NET 9+ APIs (`HybridCache`, `MemoryExtensions.Split` for spans, `Dictionary.GetAlternateLookup`, `params ReadOnlySpan<T>`) do **not** exist on .NET 8 — the references below mark the floor for each pattern, and you must downgrade to the .NET 8 fallback when the target is `net8.0`. Stephen Toub's posts are the primary benchmark source:
|
||||
[Performance Improvements in .NET 8](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/) ·
|
||||
[Performance Improvements in .NET 10](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-10/).
|
||||
|
||||
## References
|
||||
|
||||
Load on demand:
|
||||
- [references/memory-model-gc.md](references/memory-model-gc.md) — stack vs heap, generations, LOH, boxing, GC tuning
|
||||
- [references/code-patterns.md](references/code-patterns.md) — LINQ, Span/Memory, stackalloc, structs, boxing, pooling, strings (conceptual framing)
|
||||
- [references/categories.md](references/categories.md) — 14 optimization category definitions with checks and grep patterns (Mode B spine)
|
||||
- [references/grep-patterns.md](references/grep-patterns.md) — consolidated anti-pattern grep library for Phase 1 scanning
|
||||
- [references/measurement-guide.md](references/measurement-guide.md) — BenchmarkDotNet, k6, dotnet-counters, KPI targets, CI/CD
|
||||
|
||||
Pattern catalogs (with measured impact numbers, ❌/✅ pairs, and per-topic Detection recipes):
|
||||
- [references/critical-patterns.md](references/critical-patterns.md) — 17 🔴 patterns: deadlocks, order-of-magnitude regressions, excessive allocations
|
||||
- [references/async-patterns.md](references/async-patterns.md) — sync-over-async, ValueTask hot paths, Channels, false sharing
|
||||
- [references/memory-and-strings.md](references/memory-and-strings.md) — `u8` literals, `Span.Split`/`TryWrite`, compound `+=`, chained `.Replace()`
|
||||
- [references/collections-and-linq.md](references/collections-and-linq.md) — `FrozenDictionary`, `GetAlternateLookup`, `CollectionsMarshal.GetValueRefOrAddDefault`, hoisting static data
|
||||
- [references/regex-patterns.md](references/regex-patterns.md) — `[GeneratedRegex]`, `IsMatch`, `EnumerateMatches`, `NonBacktracking`
|
||||
- [references/io-and-serialization.md](references/io-and-serialization.md) — `HttpCompletionOption.ResponseHeadersRead`, `useAsync` `FileStream`, `Memory<byte>` overloads
|
||||
- [references/structural-patterns.md](references/structural-patterns.md) — sealed-class devirtualization (absence pattern, scale-based severity)
|
||||
|
||||
Reference loading guide for Mode B by signal:
|
||||
|
||||
| Signal in Code | Load |
|
||||
|---|---|
|
||||
| `async`, `await`, `Task`, `ValueTask` | `async-patterns.md` |
|
||||
| `Span<`, `Memory<`, `stackalloc`, `string.Substring`, `+=` in loops, `params` | `memory-and-strings.md` |
|
||||
| `Regex`, `[GeneratedRegex]`, `Regex.Match`, `RegexOptions.Compiled` | `regex-patterns.md` |
|
||||
| `Dictionary<`, `List<`, `.ToList()`, LINQ chains, `static readonly Dictionary<` | `collections-and-linq.md` |
|
||||
| `JsonSerializer`, `HttpClient`, `Stream`, `FileStream` | `io-and-serialization.md` |
|
||||
| Any code review on a hot path | always check `critical-patterns.md` first |
|
||||
| Codebase-wide scans (sealed classes, static `Dictionary` → `FrozenDictionary`) | `structural-patterns.md` |
|
||||
|
||||
## Iron Rule
|
||||
|
||||
**Always measure first, change second, and re-measure third.**
|
||||
|
||||
Never claim an optimization without before/after evidence from the same scenario.
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "This is obviously slow" | The runtime, JIT, and libraries often invalidate intuition. |
|
||||
| "struct means stack" | Value types are stored inline — not always on the stack. |
|
||||
| "All LINQ is slow" | .NET 9+ improved many LINQ paths. Measure before rewriting. |
|
||||
| "GC.Collect will fix it" | Forced collection treats symptoms, not cause. |
|
||||
| "Too small to matter" | MEDIUM+ impact is cumulative across the request pipeline. |
|
||||
| "I'll change the DI lifetime while I'm here" | DI lifetime changes require explicit user approval. |
|
||||
| "Need to refactor to optimize" | Optimization fixes must be surgical. Refactoring is a separate task. |
|
||||
| "Tests pass so fix is correct" | Tests passing = behavior preserved. Still verify the metric improved. |
|
||||
|
||||
## Mode Selection
|
||||
|
||||
| Situation | Mode |
|
||||
|---|---|
|
||||
| Live process: slow, high CPU/memory, hung, deadlocked, GC pauses | **A – Diagnostic** |
|
||||
| Asking how GC, heap, boxing, or LINQ overhead works in .NET | **A – Diagnostic** (conceptual) |
|
||||
| Code to analyze for anti-patterns, then fix | **B – Code Review** |
|
||||
| Both a running process AND code to fix | Start with **A**, then **B** on hot paths identified |
|
||||
|
||||
**Not for:** Visual Studio, Rider, PerfView, speedscope, or GUI-first workflows.
|
||||
|
||||
---
|
||||
|
||||
## Mode A: Diagnostic (Live Process)
|
||||
|
||||
### Investigation Order
|
||||
|
||||
1. **`dotnet-counters`** — always start here for live triage.
|
||||
2. **`dotnet-stack`** — immediately if process is stuck, hung, or deadlocked.
|
||||
3. **`dotnet-trace`** — if CPU or allocation hot paths matter.
|
||||
4. **`dotnet-gcdump`** — if heap growth matters more than call paths.
|
||||
5. **`dotnet-dump`** — if SOS heap inspection or postmortem analysis is needed.
|
||||
6. After live evidence identifies a candidate routine, apply patterns from [references/code-patterns.md](references/code-patterns.md) and [references/memory-model-gc.md](references/memory-model-gc.md).
|
||||
7. Use BenchmarkDotNet if the change is isolated and needs microbenchmark comparison.
|
||||
8. Re-run the original live capture to prove the real workload improved.
|
||||
|
||||
### CLI Tool Selection
|
||||
|
||||
| Question | Tool | What it answers |
|
||||
|---|---|---|
|
||||
| Is the process allocating, GCing, or saturating CPU? | `dotnet-counters` | Live counters and trend direction |
|
||||
| Is the process hung or deadlocked right now? | `dotnet-stack` | Current managed stack snapshot |
|
||||
| Which call paths consume CPU or allocate heavily? | `dotnet-trace` | Sampled execution and runtime events |
|
||||
| Which object types dominate managed heap? | `dotnet-gcdump` | Heap composition and type totals |
|
||||
| Need SOS heap inspection or thread state? | `dotnet-dump` | Full dump plus CLI analysis |
|
||||
| Did a code change improve an isolated routine? | BenchmarkDotNet | Reproducible microbenchmark comparison |
|
||||
|
||||
### Minimal CLI Commands
|
||||
|
||||
```bash
|
||||
dotnet-counters monitor -p <PID> --counters System.Runtime
|
||||
dotnet-counters monitor -n <ProcessName> --counters System.Runtime,Microsoft.AspNetCore.Hosting
|
||||
dotnet-stack report -p <PID>
|
||||
dotnet-trace collect -p <PID> --duration 00:00:30
|
||||
dotnet-trace report <trace.nettrace> topN
|
||||
dotnet-gcdump collect -p <PID>
|
||||
dotnet-gcdump report <file.gcdump>
|
||||
dotnet-dump collect -p <PID> --type Heap
|
||||
dotnet-dump analyze <dump> -c "dumpheap -stat" -c "exit"
|
||||
```
|
||||
|
||||
Minimal BenchmarkDotNet pattern:
|
||||
```csharp
|
||||
[MemoryDiagnoser]
|
||||
[SimpleJob(RuntimeMoniker.Net90)]
|
||||
public class CandidateBench
|
||||
{
|
||||
[Benchmark(Baseline = true)]
|
||||
public int Original() => OriginalImpl();
|
||||
|
||||
[Benchmark]
|
||||
public int Candidate() => CandidateImpl();
|
||||
}
|
||||
```
|
||||
```bash
|
||||
dotnet run -c Release
|
||||
```
|
||||
|
||||
### Reference Loading Guide
|
||||
|
||||
| User question | Load first |
|
||||
|---|---|
|
||||
| "How do stack and heap really work in .NET?" | `references/memory-model-gc.md` |
|
||||
| "Why is GC pausing or why is LOH churn hurting us?" | `references/memory-model-gc.md` |
|
||||
| "How should I optimize this LINQ?" | `references/code-patterns.md` |
|
||||
| "Can I move this to the stack with stackalloc or Span?" | `references/code-patterns.md` |
|
||||
| "Should this be a struct, ref struct, readonly struct, or class?" | Both |
|
||||
| "Why is this boxing?" | `references/code-patterns.md` |
|
||||
|
||||
### Diagnostic Output
|
||||
|
||||
Report: measured symptom + evidence (counter values, trace hotspots, heap stats) · chosen tool and why · relevant tradeoff (allocation vs copy, deferred vs eager, stack vs pool) · before/after result, or explicitly state if still unverified.
|
||||
|
||||
---
|
||||
|
||||
## Mode B: Code Review (Static Analysis)
|
||||
|
||||
### Target
|
||||
|
||||
`$ARGUMENTS` is the optimization target:
|
||||
- **File path**: Analyze that file and its close dependencies.
|
||||
- **Directory**: Analyze all C# files in that directory.
|
||||
- **"all"**: Scan the solution with Grep, deep-dive the worst offenders.
|
||||
- **`--fix`** anywhere: Skip confirmation and apply fixes after analysis.
|
||||
- **Empty**: Check `git diff --name-only HEAD~5 -- '*.cs'` for recently changed files. If none, ask the user.
|
||||
|
||||
### Phase 1: Discovery
|
||||
|
||||
1. **Glob** to find `.cs` files matching the target.
|
||||
2. **Read** file contents. For files under 500 lines, read the whole file first — visual inspection catches patterns faster than grep, then grep confirms counts.
|
||||
3. **Detect signals** in the code (async, Span, Regex, Dictionary, JsonSerializer, etc.) and **load matching pattern catalogs** from the per-topic references listed at the top of this file.
|
||||
4. **Grep** for anti-patterns. Run the recipes in [references/grep-patterns.md](references/grep-patterns.md) plus the per-topic Detection sections in the catalogs you loaded.
|
||||
5. **Emit a scan execution checklist** before classifying — list each recipe and the hit count. **0 hits is valid and valuable** (confirms good practice).
|
||||
|
||||
### Phase 2: Analysis (Read-Only)
|
||||
|
||||
Check each file against all 14 categories. Record per finding: **file path, line number, current pattern, recommended pattern, impact level, category**.
|
||||
|
||||
Read [references/categories.md](references/categories.md) for detailed check definitions.
|
||||
|
||||
#### Compound Allocation Check
|
||||
|
||||
Single-line grep recipes miss multi-allocation patterns. After running scan recipes, look for:
|
||||
|
||||
1. **Branched `.Replace()` chains** — methods that call `.Replace()` across multiple `if/else` branches. Report total allocation count across all branches, not just per-line.
|
||||
2. **Cross-method chaining** — public method A calls B (which does 3 regex replaces) then calls C (which allocates). Report the total chain cost as one finding, not per-method.
|
||||
3. **Compound `+=` with embedded allocating calls** — `result += $"...{Foo().ToLower()}"` is 2+ allocations (interpolation + `ToLower` + concatenation). Flag the compound cost, not just `.ToLower()`.
|
||||
4. **`string.Format` specificity** — distinguish resource-loaded format strings (not fixable) from compile-time literal format strings (fixable with interpolation). Enumerate only the actionable sites.
|
||||
|
||||
#### Cross-File Consistency Check
|
||||
|
||||
If an optimized pattern is found in one file, check whether sibling files (same directory, same interface, same base class) use the un-optimized equivalent. Flag as MEDIUM with the optimized file as evidence.
|
||||
|
||||
#### Verify-the-Inverse Rule
|
||||
|
||||
For absence patterns (e.g., unsealed classes, static `Dictionary` not converted to `FrozenDictionary`, `RegexOptions.Compiled` not migrated to `[GeneratedRegex]`), always count both sides and report the **N-of-M ratio**, not just the count of bad cases. The ratio determines severity:
|
||||
|
||||
- 0/185 sealed → systematic codebase-wide issue
|
||||
- 12/15 sealed → consistency fix on the remaining 3
|
||||
- 50/100 sealed → mid-migration; flag the laggards
|
||||
|
||||
| # | Category | Code | Focus |
|
||||
|---|---|---|---|
|
||||
| 1 | Memory Allocation | MEM | Span, ArrayPool, pooling, stackalloc, string optimization, collections |
|
||||
| 2 | Async Anti-Patterns | ASYNC | Blocking, ValueTask, CancellationToken, IAsyncEnumerable, Channel |
|
||||
| 3 | LINQ Inefficiencies | LINQ | Count vs Any, multiple enumeration, filter/project order |
|
||||
| 4 | Database | DB | EF Core, CosmosDB patterns, N+1, partition keys, RU cost |
|
||||
| 5 | JSON Serialization | JSON | Options reuse, source generators, serializer boundaries |
|
||||
| 6 | Caching | CACHE | HybridCache, stampede protection, output cache, size limits |
|
||||
| 7 | DI Lifetimes | DI | Captive dependencies, lifetime mismatches, IOptions patterns |
|
||||
| 8 | Concurrency | CONC | Lock contention, throttling, thread safety, Channel patterns |
|
||||
| 9 | HttpClient | HTTP | IHttpClientFactory, resilience, response disposal |
|
||||
| 10 | Exception Control Flow | EXC | Try/catch for expected paths, broad catches |
|
||||
| 11 | Response Optimization | RESP | Compression, pagination, ETags |
|
||||
| 12 | String Optimization | STR | Concatenation loops, ToLower/ToUpper, String.Format |
|
||||
| 13 | Startup & Pipeline | STARTUP | Middleware ordering, compression, health checks, PGO |
|
||||
| 14 | Metrics & Observability | METRICS | IMeterFactory, histograms, tag cardinality, OpenTelemetry |
|
||||
|
||||
### Phase 3: Report
|
||||
|
||||
```
|
||||
## Performance Analysis Report
|
||||
|
||||
### Summary
|
||||
- Files analyzed: N
|
||||
- Total findings: N
|
||||
- Critical (HIGH): N | Moderate (MEDIUM): N | Minor (LOW): N
|
||||
|
||||
### Findings by Category
|
||||
|
||||
#### [CATEGORY_NAME] (N findings)
|
||||
|
||||
| # | Impact | File:Line | Issue | Recommendation |
|
||||
|---|--------|-----------|-------|----------------|
|
||||
| 1 | HIGH | `path/File.cs:42` | Current anti-pattern | Recommended fix |
|
||||
|
||||
### Prioritized Action List
|
||||
1. [HIGH] Fix blocking async calls in X — thread pool starvation risk
|
||||
2. [MEDIUM] Switch to ArrayPool in Z — reduces GC pressure on upload path
|
||||
```
|
||||
|
||||
**Impact levels:**
|
||||
- **HIGH**: Measurable gain, prevents starvation, fixes correctness, reduces P95 latency. Examples: blocking async, missing CancellationToken, N+1 queries, captive dependencies.
|
||||
- **MEDIUM**: Reduces allocations, GC pressure, or unnecessary work. Examples: ArrayPool, StringBuilder, FrozenDictionary.
|
||||
- **LOW**: Minor improvements, cold-path optimizations. Examples: initial collection capacity, Count() vs Any().
|
||||
|
||||
**Scale-based severity escalation.** When the same anti-pattern appears across many instances, escalate:
|
||||
|
||||
- 1–10 instances → report at the pattern's base severity
|
||||
- 11–50 instances → escalate LOW patterns to MEDIUM
|
||||
- 50+ instances → MEDIUM with elevated priority; flag as a codebase-wide systematic issue
|
||||
|
||||
Always report **exact counts from scan recipes**, not estimates. Group findings by severity (HIGH → MEDIUM → LOW), not by file. Merge related findings that share the same fix (e.g., all `.ToLower()` calls in one finding, not split per file).
|
||||
|
||||
### Phase 4: Optimization (Apply Fixes)
|
||||
|
||||
After presenting the report:
|
||||
- If `--fix` in `$ARGUMENTS`, proceed directly.
|
||||
- Otherwise ask: "Would you like me to apply these optimizations? I'll work one category at a time, starting with HIGH impact. You can specify categories or findings (e.g., 'fix ASYNC and MEM' or 'fix #1, #3')."
|
||||
|
||||
**Before any fix:**
|
||||
1. **Read actual code context** around the grep match — false positives exist (`.Result` in `Task.FromResult` is NOT blocking).
|
||||
2. Confirm the finding is real. If uncertain, flag as "needs manual review."
|
||||
|
||||
**Applying fixes:**
|
||||
1. One category at a time, highest impact first. Use Edit tool with brief before/after summary.
|
||||
2. After each category: `dotnet build --no-restore`
|
||||
3. After all changes: `dotnet test`
|
||||
4. If build or tests fail, diagnose before continuing.
|
||||
|
||||
---
|
||||
|
||||
## Analyzer Radar
|
||||
|
||||
- `CA1826`, `CA1827`, `CA1829`, `CA1836`, `CA1851`, `CA1860` — LINQ and enumeration
|
||||
- `CA1845`, `CA1846`, `CA1858` — string and span-friendly APIs
|
||||
- `CA1834`, `CA1865`–`CA1867` — StringBuilder char overloads
|
||||
- `CA1870` — cached `SearchValues<T>`
|
||||
|
||||
These are clues, not goals. Apply where measured hot paths justify it.
|
||||
|
||||
## Pattern Guardrails
|
||||
|
||||
- Do not say "put it on the stack" as a blanket goal. Explain lifetime, copies, boxing, and escape rules.
|
||||
- Do not suggest `stackalloc` for unbounded sizes, large buffers, or loop-carried allocations.
|
||||
- Do not recommend `Span<T>` for data that crosses `await`, escapes to the heap, or lives in object fields — use `Memory<T>`.
|
||||
- Do not recommend converting every `class` to a `struct` — large, mutable, or frequently boxed types often get worse.
|
||||
- Do not blanket-rewrite LINQ to loops — use analyzer-backed fixes first.
|
||||
- Do not recommend pooling without ownership rules — returned pooled arrays must not be reused by the caller.
|
||||
- Do not recommend `GC.Collect()` except for rare justified lifecycle boundaries with measurement.
|
||||
|
||||
## Red Flags — STOP and Confirm
|
||||
|
||||
Stop and ask before:
|
||||
- Changing `Program.cs` or the middleware pipeline
|
||||
- Adding a new NuGet package
|
||||
- Changing any DI service lifetime registration
|
||||
- Replacing the serializer in the HTTP pipeline
|
||||
- Modifying API response shapes or route patterns
|
||||
- Changing error handling patterns
|
||||
|
||||
## Constraints
|
||||
|
||||
- NEVER add NuGet packages without user approval
|
||||
- NEVER change DI lifetimes without explaining implications and getting confirmation
|
||||
- NEVER modify `Program.cs` or middleware pipeline without explicit approval
|
||||
- NEVER change API contracts, route patterns, or response shapes
|
||||
- ALWAYS preserve existing tests; update only if behavior intentionally changes
|
||||
- ALWAYS use the Grep tool for searches, never bash `grep` or `find`
|
||||
|
||||
Consult the project's CLAUDE.md or AGENTS.md for project-specific rules and constraints.
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
# Async & Concurrency Patterns
|
||||
|
||||
### Don't Expose Async Wrappers for Sync Methods
|
||||
🟡 **AVOID** wrapping sync methods with `Task.Run` in libraries | .NET Core+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
public Task<int> ComputeHashAsync(byte[] data) =>
|
||||
Task.Run(() => ComputeHash(data));
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
public int ComputeHash(byte[] data) { /* CPU-bound work */ }
|
||||
// Consumer decides: var hash = await Task.Run(() => lib.ComputeHash(data));
|
||||
```
|
||||
|
||||
**Impact: Eliminates unnecessary thread pool queue/dequeue overhead per call.**
|
||||
|
||||
### Don't Expose Sync Wrappers for Async Methods
|
||||
🟡 **AVOID** creating sync wrappers that block on async implementations | .NET Core+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
public string GetData() => GetDataAsync().Result;
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
public async Task<string> GetDataAsync() { /* ... */ }
|
||||
```
|
||||
|
||||
**Impact: Prevents deadlocks and thread pool starvation from hidden sync-over-async blocking.**
|
||||
|
||||
### Use ValueTask for Hot Paths with Frequent Sync Completion
|
||||
🟡 **DO** use `ValueTask<T>` on hot paths where sync completion is common | .NET Core 2.1+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
public async Task<int> ReadAsync(Memory<byte> buffer)
|
||||
{
|
||||
if (_bufferedCount > 0)
|
||||
return ReadFromBuffer(buffer.Span);
|
||||
return await ReadAsyncCore(buffer);
|
||||
}
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
public ValueTask<int> ReadAsync(Memory<byte> buffer)
|
||||
{
|
||||
if (_bufferedCount > 0)
|
||||
return new ValueTask<int>(ReadFromBuffer(buffer.Span));
|
||||
return new ValueTask<int>(ReadAsyncCore(buffer));
|
||||
}
|
||||
```
|
||||
|
||||
**Impact: Eliminates Task\<T\> allocation on synchronous completion — the struct stores results inline.**
|
||||
|
||||
### Use Channels for Producer/Consumer
|
||||
🟡 **DO** use `System.Threading.Channels` for producer-consumer patterns | .NET Core 3.0+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
var queue = new BlockingCollection<WorkItem>();
|
||||
var item = queue.Take();
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
var channel = Channel.CreateUnbounded<WorkItem>();
|
||||
|
||||
// Producer
|
||||
await channel.Writer.WriteAsync(item);
|
||||
|
||||
// Consumer
|
||||
await foreach (var item in channel.Reader.ReadAllAsync())
|
||||
Process(item);
|
||||
```
|
||||
|
||||
**Impact: ~25% faster, ~95% fewer GC collections vs manual approaches.**
|
||||
|
||||
### Avoid False Sharing with Thread-Local State
|
||||
🟡 **AVOID** adjacent mutable fields written by different threads | .NET 7+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
class SharedCounters
|
||||
{
|
||||
public long Counter1;
|
||||
public long Counter2;
|
||||
}
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
[StructLayout(LayoutKind.Explicit, Size = 128)]
|
||||
struct PaddedCounter
|
||||
{
|
||||
[FieldOffset(0)] public long Value;
|
||||
}
|
||||
```
|
||||
|
||||
**Impact: Eliminates cross-core cache invalidation — can improve multi-threaded throughput by 10x+.**
|
||||
|
||||
## Detection
|
||||
|
||||
Scan recipes for async anti-patterns. Run these and report exact counts.
|
||||
|
||||
```bash
|
||||
# async void methods (correctness issue — crashes on exception)
|
||||
grep -rn --include='*.cs' 'async void' --exclude-dir=bin --exclude-dir=obj . | grep -v 'event' | wc -l
|
||||
```
|
||||
|
||||
### Patterns Requiring Manual Review
|
||||
|
||||
- **Sync-over-async** (`.Result`, `.Wait()`): `.Result` matches any property named Result — needs type context to confirm it's `Task.Result`
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
# Optimization Category Definitions
|
||||
|
||||
Complete check definitions for all 14 optimization categories. Each category lists specific checks to perform, with grep patterns at the end for automated scanning.
|
||||
|
||||
---
|
||||
|
||||
## Category 1: Memory Allocation (MEM)
|
||||
|
||||
Checks:
|
||||
|
||||
- **Unnecessary string allocations**: `Substring()` calls that could use `Span<char>` or `AsSpan()`. String concatenation with `+` inside loops (should use `StringBuilder` or `string.Create`).
|
||||
- **Missing ArrayPool/MemoryPool usage**: `new byte[...]` for temporary buffers, especially in I/O paths. Should use `ArrayPool<byte>.Shared.Rent()` with try/finally Return.
|
||||
- **Large Object Heap triggers**: Allocations of objects >= 85,000 bytes (arrays, large strings, `MemoryStream` without `RecyclableMemoryStream`).
|
||||
- **Missing object pooling**: Frequently created/disposed objects (like `StringBuilder`) that could use `ObjectPool<T>`.
|
||||
- **Record class vs record struct**: Small, immutable DTOs that are `record class` but could be `readonly record struct` to avoid heap allocation.
|
||||
- **Boxing**: Value types cast to `object` or non-generic interfaces. Structs without `IEquatable<T>`.
|
||||
- **Collection inefficiencies**: `new List<T>()` or `new Dictionary<K,V>()` without initial capacity when size is known or estimable. Double-lookup patterns (`TryGetValue` + indexer set) that could use `CollectionsMarshal.GetValueRefOrAddDefault`. Read-only dictionaries populated once that could be `FrozenDictionary<K,V>` (.NET 8+).
|
||||
- **stackalloc for small buffers**: Flag `new byte[N]` where N <= 256 in synchronous methods. Recommend `Span<byte> buffer = stackalloc byte[N]` for short-lived stack allocation with zero GC pressure.
|
||||
- **string.Create for pre-sized construction**: When output string length is known at call time, `string.Create(length, state, action)` avoids intermediate allocations by writing directly into the final buffer.
|
||||
- **Interpolated strings in logging** *(Impact: LOW — only matters when the log level is inactive at runtime)*: `_logger.LogXxx($"...")` allocates the interpolated string even when the log level is disabled. Use structured logging parameters `_logger.LogXxx("Message {Param}", value)` or the `[LoggerMessage]` source generator for high-frequency hot paths.
|
||||
- **ReadOnlySpan for string parsing**: Flag `.Split()` and `.Substring()` in hot paths where `AsSpan()` slicing avoids allocation. Common in string parsing, normalization, and URL handling.
|
||||
- **CollectionsMarshal.GetValueRefOrAddDefault**: Flag the TryGetValue + indexer set double-lookup pattern. Single-lookup alternative reduces dictionary operations by 50%.
|
||||
- **RecyclableMemoryStream**: Flag `new MemoryStream()` in I/O-heavy paths (blob upload/download, log writes). `Microsoft.IO.RecyclableMemoryStream` pools internal buffers and avoids LOH fragmentation.
|
||||
|
||||
Grep patterns:
|
||||
```
|
||||
\.Substring\(
|
||||
new byte\[
|
||||
new MemoryStream\(\)
|
||||
new StringBuilder\(\)
|
||||
new List<.*>\(\)
|
||||
new Dictionary<.*>\(\)
|
||||
_logger\.Log(Debug|Trace|Information|Warning|Error|Critical)\(\$"
|
||||
\.Split\(
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Category 2: Async Anti-Patterns (ASYNC)
|
||||
|
||||
Checks:
|
||||
|
||||
- **Blocking async calls**: `.Result`, `.Wait()`, `.GetAwaiter().GetResult()` -- causes thread pool starvation. CRITICAL finding.
|
||||
- **async void**: Methods declared `async void` (except event handlers) -- swallows exceptions and cannot be awaited.
|
||||
- **Missing CancellationToken**: Async methods that do not accept or propagate `CancellationToken`. Every async method should have `CancellationToken cancellationToken = default`.
|
||||
- **Missing ValueTask**: Methods that frequently return cached/synchronous results but use `Task<T>` instead of `ValueTask<T>`. Look for `if (cache.TryGetValue(...)) return Task.FromResult(...)`. Benchmark data: ValueTask 7.41ns/0B vs Task 15.23ns/72B.
|
||||
- **Sequential awaits that could parallelize**: Multiple independent `await` calls in sequence that could use `Task.WhenAll`.
|
||||
- **Async over sync**: Methods that use `Task.Run` to wrap synchronous code in an ASP.NET Core context (unnecessary and wastes a thread).
|
||||
- **ConfigureAwait(false) in library projects**: LOW/INFO level. Not required in ASP.NET Core (no sync context), but recommended if library assemblies may be reused outside ASP.NET Core.
|
||||
- **IAsyncEnumerable opportunities**: Methods returning `Task<List<T>>` where the caller iterates sequentially. If the caller processes items one-by-one, `IAsyncEnumerable<T>` reduces memory and improves time-to-first-byte.
|
||||
- **Channel verification**: Verify `BoundedChannelOptions` has `SingleReader`/`SingleWriter` hints set for performance. Verify `CancellationToken` is propagated on `WriteAsync` and `ReadAllAsync`.
|
||||
|
||||
Grep patterns:
|
||||
```
|
||||
\.Result[^s]
|
||||
\.Wait\(\)
|
||||
\.GetAwaiter\(\)\.GetResult\(\)
|
||||
async void
|
||||
Task\.Run\(
|
||||
\.WriteAsync\([^,]*\)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Category 3: LINQ Inefficiencies (LINQ)
|
||||
|
||||
Checks:
|
||||
|
||||
- **Count() > 0 or Count() == 0**: Should use `Any()` or `!Any()`. `Count()` may enumerate the entire collection.
|
||||
- **Multiple enumeration**: An `IEnumerable<T>` variable used more than once without materializing.
|
||||
- **Filter after projection**: `.Select(...).Where(...)` -- should filter first, then project.
|
||||
- **ToList() too early**: `.ToList().Where(...)` or `.ToList().Select(...)` -- materializes before filtering.
|
||||
- **OrderBy before Where**: Sorting the full collection before filtering it down.
|
||||
|
||||
Grep patterns:
|
||||
```
|
||||
\.Count\(\) [><=!]
|
||||
\.ToList\(\)\.Where\(
|
||||
\.ToList\(\)\.Select\(
|
||||
\.Select\(.*\)\.Where\(
|
||||
\.OrderBy.*\.Where\(
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Category 4: Database (DB)
|
||||
|
||||
Check for both EF Core and CosmosDB patterns depending on what the project uses. Consult the project's CLAUDE.md or AGENTS.md for the data access strategy.
|
||||
|
||||
Checks:
|
||||
|
||||
- **N+1 queries**: Loops that call the database inside each iteration.
|
||||
- **Missing AsNoTracking**: EF Core read-only queries without `.AsNoTracking()`.
|
||||
- **Missing compiled queries**: Frequently executed EF Core queries on hot paths without `EF.CompileAsyncQuery`.
|
||||
- **Full entity loading**: Fetching entire entities when only a few fields are needed (should project to DTOs).
|
||||
- **Missing AsSplitQuery**: EF Core queries with multiple `.Include()` calls without `.AsSplitQuery()`.
|
||||
- **CosmosDB partition key misuse**: Operations not specifying the partition key, or using cross-partition queries unnecessarily.
|
||||
- **CosmosDB point reads**: Using queries instead of `ReadItemAsync` when both `id` and partition key are known.
|
||||
- **RU cost awareness**: Flag discarded `ItemResponse<T>` without logging `RequestCharge`. Recommend tracking RU cost via metrics for cost visibility.
|
||||
- **Cross-partition query detection**: `GetItemLinqQueryable()` without partition key option leads to fan-out queries. Verify all LINQ queryables specify the partition key.
|
||||
- **Indexing policy review**: Flag if queries filter on fields that likely lack composite indexes.
|
||||
- **Redundant round-trips**: Flag patterns where a query fetches an ID, then a separate point read fetches the full document. Recommend a single query.
|
||||
- **EnableContentResponseOnWrite = false**: On write operations where the response body is not needed, setting this option reduces RU cost.
|
||||
|
||||
Grep patterns:
|
||||
```
|
||||
\.Include\(.*\.Include\(
|
||||
await.*foreach.*await.*Async
|
||||
ReadItemAsync
|
||||
GetItemQueryIterator
|
||||
GetItemLinqQueryable
|
||||
\.RequestCharge
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Category 5: JSON Serialization (JSON)
|
||||
|
||||
Checks:
|
||||
|
||||
- **New JsonSerializerOptions per call**: `new JsonSerializerOptions { ... }` inside method bodies -- rebuilds the metadata cache every time. Should use a static readonly instance.
|
||||
- **Missing source generators**: High-throughput serialization paths without `[JsonSerializable]` source generation context.
|
||||
- **Newtonsoft.Json in hot paths**: If the project uses Newtonsoft.Json for MVC, flag any hot-path internal serialization that could benefit from `System.Text.Json` with source generators. NEVER suggest replacing the controller/DTO serializer without checking the project's documented constraints.
|
||||
- **System.Text.Json source generators for internal serialization**: Internal paths (database serialization, audit logs, blob metadata) that don't affect API contracts are candidates for `System.Text.Json` with source generators.
|
||||
- **JsonSerializerSettings singleton**: Flag `new JsonSerializerSettings()` in method bodies. The contract resolver cache is rebuilt each time. Use a static readonly instance or inject via DI.
|
||||
- **CosmosDB SDK serializer**: The Cosmos SDK supports custom serializers. `CosmosSystemTextJsonSerializer` with source generators reduces allocation on read/write operations.
|
||||
|
||||
Grep patterns:
|
||||
```
|
||||
new JsonSerializerOptions
|
||||
JsonConvert\.Serialize
|
||||
JsonConvert\.Deserialize
|
||||
new JsonSerializer
|
||||
new JsonSerializerSettings
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Category 6: Caching (CACHE)
|
||||
|
||||
Checks:
|
||||
|
||||
- **Repeated expensive calls without caching**: Service methods that call external APIs on every request without caching the result.
|
||||
- **Missing HybridCache pattern**: Look for manual cache-aside patterns that could use `HybridCache` for built-in stampede protection, two-level caching (L1 memory + L2 distributed), and tag invalidation. **HybridCache requires .NET 10 (or .NET 9) — the `Microsoft.Extensions.Caching.Hybrid` package does not target .NET 8.** On .NET 8 implement `IMemoryCache` (L1) + `IDistributedCache` (L2) manually with a `SemaphoreSlim` keyed by cache key for stampede protection. See [HybridCache GA announcement](https://devblogs.microsoft.com/dotnet/hybrid-cache-is-now-ga/).
|
||||
- **Static data fetched repeatedly**: Configuration, taxonomies, or lookup data fetched from external APIs that rarely changes.
|
||||
- **Missing output caching**: Read-only GET endpoints that return the same data for all callers -- candidates for `[OutputCache]`.
|
||||
- **HybridCache upgrade path (.NET 10 only)**: Manual L1+L2 caching with `SemaphoreSlim` stampede protection can migrate to `HybridCache` `GetOrCreateAsync` with built-in stampede protection and tag invalidation **when the project target is `net10.0`**. On `net8.0` the manual pattern is the correct end state, not a stepping stone.
|
||||
- **Cache stampede detection**: Cache-aside without locking -- `TryGetValue` followed by expensive call followed by `Set` without `SemaphoreSlim` or equivalent stampede guard.
|
||||
- **Tag-based invalidation with RemoveByTagAsync**: When using HybridCache, group related entries by tag for efficient bulk invalidation instead of tracking individual keys.
|
||||
- **IMemoryCache size limits**: Flag `AddMemoryCache()` without `SizeLimit` in `MemoryCacheOptions`. Unbounded in-memory cache can grow until the process runs out of memory.
|
||||
|
||||
Grep patterns:
|
||||
```
|
||||
GetAsync\(
|
||||
SendAsync\(
|
||||
_cache\.TryGetValue
|
||||
DistributedCache
|
||||
AddMemoryCache\(\)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Category 7: DI Lifetime Issues (DI)
|
||||
|
||||
Consult the project's CLAUDE.md or AGENTS.md for the expected DI lifetime registrations.
|
||||
|
||||
Checks:
|
||||
|
||||
- **Transient services that should be Singleton**: Stateless, thread-safe services registered as Transient that have no per-request state (could be Singleton for zero allocation).
|
||||
- **Scoped injected into Singleton**: A Scoped service captured in a Singleton constructor -- captive dependency bug.
|
||||
- **IOptions vs IOptionsMonitor vs IOptionsSnapshot**: `IOptions<T>` in Singleton services that need to react to config changes should use `IOptionsMonitor<T>`. `IOptionsSnapshot<T>` in Singleton is a captive dependency.
|
||||
|
||||
---
|
||||
|
||||
## Category 8: Concurrency Issues (CONC)
|
||||
|
||||
Checks:
|
||||
|
||||
- **Lock contention**: `lock` statements that guard async operations (should use `SemaphoreSlim`).
|
||||
- **Missing throttling**: Unbounded parallel calls to external APIs without `SemaphoreSlim` or concurrency limits.
|
||||
- **Thread-unsafe patterns**: Shared mutable state without synchronization. `HttpContext` accessed from background threads.
|
||||
- **Channel usage patterns**: If the project uses `Channel<T>` for background tasks, verify `SingleReader`/`SingleWriter` hints are set correctly for performance.
|
||||
|
||||
Grep patterns:
|
||||
```
|
||||
lock\s*\(
|
||||
new SemaphoreSlim
|
||||
HttpContext.*Task\.Run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Category 9: HttpClient Misuse (HTTP)
|
||||
|
||||
Checks:
|
||||
|
||||
- **new HttpClient()**: Direct instantiation instead of `IHttpClientFactory`. Causes socket exhaustion and DNS caching issues.
|
||||
- **Missing resilience**: HTTP calls without retry/circuit-breaker policies. Verify `Microsoft.Extensions.Http.Resilience` or Polly is applied to external API clients.
|
||||
- **Missing response disposal**: `HttpResponseMessage` not disposed after reading.
|
||||
|
||||
Grep patterns:
|
||||
```
|
||||
new HttpClient\(
|
||||
new HttpClient\b
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Category 10: Exception-Driven Control Flow (EXC)
|
||||
|
||||
Checks:
|
||||
|
||||
- **Try/catch for expected paths**: Using exceptions for normal control flow (e.g., catching `KeyNotFoundException` instead of `TryGetValue`, catching `FormatException` instead of `TryParse`).
|
||||
- **Broad catch blocks**: `catch (Exception)` that swallow errors or use exceptions as branching logic.
|
||||
- **Exception allocation in hot paths**: Throwing exceptions on paths that execute frequently.
|
||||
|
||||
Grep patterns:
|
||||
```
|
||||
catch\s*\(Exception\b
|
||||
catch\s*\(KeyNotFoundException
|
||||
catch\s*\(FormatException
|
||||
catch\s*\(InvalidOperationException
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Category 11: Response Optimization (RESP)
|
||||
|
||||
Checks:
|
||||
|
||||
- **Missing compression**: No response compression middleware, or JSON responses served uncompressed.
|
||||
- **Missing pagination**: Endpoints returning unbounded collections.
|
||||
- **Missing ETags for conditional requests**: GET endpoints without ETag support where the data has a natural version (e.g., database ETags or row versions).
|
||||
|
||||
---
|
||||
|
||||
## Category 12: String Optimization (STR)
|
||||
|
||||
Checks:
|
||||
|
||||
- **String concatenation in loops**: `+=` on strings inside `for`/`foreach`/`while` loops.
|
||||
- **String.Format in hot paths**: Could use interpolated string handlers or `StringBuilder`.
|
||||
- **Repeated string operations**: Multiple `ToLower()`/`ToUpper()` calls on the same value. Should use `StringComparison.OrdinalIgnoreCase` instead.
|
||||
|
||||
Grep patterns:
|
||||
```
|
||||
\+= "
|
||||
\+= \$"
|
||||
\.ToLower\(\)
|
||||
\.ToUpper\(\)
|
||||
String\.Format\(
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Category 13: Startup & Pipeline Optimization (STARTUP)
|
||||
|
||||
Checks:
|
||||
|
||||
- **Middleware ordering**: Verify Program.cs follows the recommended sequence: ExceptionHandler, ResponseCompression, OutputCache, Routing, RateLimiter, CORS, Authentication, Authorization, MapControllers. Incorrect ordering degrades performance (e.g., compression after routing skips static responses).
|
||||
- **Response compression**: Flag missing `AddResponseCompression`/`UseResponseCompression`. Without it, all JSON responses are uncompressed. Recommend Brotli (optimal ratio) + GZip (compatibility) providers with `EnableForHttps = true`.
|
||||
- **Health check optimization**: `.ShortCircuit()` (.NET 8+) bypasses the entire middleware pipeline for health endpoints. `.DisableHttpMetrics()` prevents health check traffic from skewing request duration metrics.
|
||||
- **PGO/ReadyToRun**: Check .csproj for `<TieredPGO>true</TieredPGO>` (dynamic PGO for runtime hot-path optimization) and `<PublishReadyToRun>true</PublishReadyToRun>` (pre-compiled code for faster startup). Both should be present for production builds.
|
||||
- **Warm-up pattern**: `ApplicationStarted` callback to warm expensive singletons (database connections, cache, external API health). Cold-start latency without warm-up can spike P99 for the first requests after deployment.
|
||||
|
||||
Grep patterns:
|
||||
```
|
||||
UseResponseCompression
|
||||
AddResponseCompression
|
||||
ShortCircuit
|
||||
AddOutputCache
|
||||
UseOutputCache
|
||||
TieredPGO
|
||||
PublishReadyToRun
|
||||
ApplicationStarted
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Category 14: Metrics & Observability (METRICS)
|
||||
|
||||
Checks:
|
||||
|
||||
- **IMeterFactory vs static new Meter()**: Services should inject `IMeterFactory` from DI rather than using `new Meter(...)`. `IMeterFactory` enables testability with `MetricCollector<T>` and proper meter lifecycle management.
|
||||
- **Missing histograms**: If the project only has `Counter<long>` instruments, recommend `Histogram<double>` for request processing duration, external API latency, and database query duration. Histograms enable percentile analysis (P50/P95/P99).
|
||||
- **Tag cardinality**: Verify metric tags have bounded cardinality. NEVER use request IDs, user IDs, or unbounded strings as metric tags. Tags like `operation_name`, `status_code`, `endpoint` are acceptable (bounded). Unbounded tags cause metric explosion and memory issues.
|
||||
- **OpenTelemetry AddMeter() registration**: Verify custom meter names are registered with `.AddMeter("YourMeterName")` in the OpenTelemetry metrics configuration. Without this, custom counters are silently dropped.
|
||||
|
||||
Grep patterns:
|
||||
```
|
||||
new Meter\(
|
||||
CreateCounter
|
||||
CreateHistogram
|
||||
AddMeter
|
||||
\.Record\(
|
||||
\.Add\(
|
||||
```
|
||||
|
|
@ -6,6 +6,8 @@ metadata:
|
|||
|
||||
# Code Patterns
|
||||
|
||||
> **See also:** for pattern-by-pattern detection recipes with measured impact numbers and ❌/✅ pairs, see the topic catalogs: [critical-patterns.md](critical-patterns.md), [async-patterns.md](async-patterns.md), [memory-and-strings.md](memory-and-strings.md), [collections-and-linq.md](collections-and-linq.md), [regex-patterns.md](regex-patterns.md), [io-and-serialization.md](io-and-serialization.md), [structural-patterns.md](structural-patterns.md). This file covers the conceptual framing (when/why), those files cover the catalog (what/how-much).
|
||||
|
||||
Use this reference after a profile or benchmark identifies a hot path. Do not apply these patterns speculatively.
|
||||
|
||||
## Table Of Contents
|
||||
|
|
@ -99,9 +101,10 @@ Keep deferred execution unless you need a snapshot, repeated traversal, indexing
|
|||
|
||||
### Do not blanket-rewrite LINQ to loops
|
||||
|
||||
- .NET 10 improved many LINQ operations substantially.
|
||||
- .NET 10 improved many LINQ operations substantially through JIT array-interface devirtualisation — operations like `Skip`/`Take`/`Sum` on arrays and `ReadOnlyCollection<T>` got roughly 50% faster *for free*. See Stephen Toub, [Performance Improvements in .NET 10](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-10/).
|
||||
- On `net8.0` LINQ has the historical performance characteristics described in [Performance Improvements in .NET 8](https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/) — fast paths exist for `Count`, `ToList`, `ToArray` on `ICollection<T>`, but indexed access via `ElementAt`/`Skip`/`Take` is not as cheap as on .NET 10.
|
||||
- Start with analyzer-backed fixes and measurement.
|
||||
- Replace LINQ with hand-written loops only when a benchmark or trace shows that the remaining cost matters.
|
||||
- Replace LINQ with hand-written loops only when a benchmark or trace shows that the remaining cost matters — on either target.
|
||||
|
||||
### Use `TryGetNonEnumeratedCount` when count is optional
|
||||
|
||||
|
|
@ -0,0 +1,217 @@
|
|||
# Collections & LINQ Patterns
|
||||
|
||||
### Use FrozenDictionary/FrozenSet for Read-Heavy Lookup Tables
|
||||
🟡 **DO** use `FrozenDictionary`/`FrozenSet` for collections created once and read many times | .NET 8+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
private static readonly Dictionary<string, int> s_statusCodes = new()
|
||||
{
|
||||
["OK"] = 200, ["NotFound"] = 404, ["InternalServerError"] = 500
|
||||
};
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
private static readonly FrozenDictionary<string, int> s_statusCodes =
|
||||
new Dictionary<string, int>
|
||||
{
|
||||
["OK"] = 200, ["NotFound"] = 404, ["InternalServerError"] = 500
|
||||
}.ToFrozenDictionary();
|
||||
```
|
||||
|
||||
**Impact: ~50% faster lookups than Dictionary, ~14x faster than ImmutableDictionary.**
|
||||
|
||||
### Use Dictionary Alternate Lookup for Span-Based Keys
|
||||
🟡 **DO** use `GetAlternateLookup<ReadOnlySpan<char>>()` to avoid string allocation on lookups | **.NET 10 (or .NET 9) only — NOT available on .NET 8**
|
||||
|
||||
❌ (allocates on every lookup; the only option on .NET 8)
|
||||
```csharp
|
||||
string key = headerLine.Substring(0, colonIndex);
|
||||
if (s_dict.TryGetValue(key, out int value)) { /* ... */ }
|
||||
```
|
||||
✅ .NET 10 / C# 14
|
||||
```csharp
|
||||
var lookup = s_dict.GetAlternateLookup<ReadOnlySpan<char>>();
|
||||
ReadOnlySpan<char> key = headerLine.AsSpan(0, colonIndex);
|
||||
if (lookup.TryGetValue(key, out int value)) { }
|
||||
```
|
||||
✅ .NET 8 fallback — keep the allocation but minimise it
|
||||
```csharp
|
||||
// On net8.0 GetAlternateLookup does not exist (added in .NET 9 BCL).
|
||||
// Pre-intern frequent keys, or accept the allocation. If the hot path is
|
||||
// truly critical, store keys as ReadOnlyMemory<char> and write a custom
|
||||
// IEqualityComparer<string> that compares against a span via string.Compare.
|
||||
string key = headerLine.Substring(0, colonIndex);
|
||||
if (s_dict.TryGetValue(key, out int value)) { /* ... */ }
|
||||
```
|
||||
|
||||
**Impact: Avoids string allocation per lookup on .NET 10 — especially valuable in parser/protocol hot paths.**
|
||||
|
||||
### Use CollectionsMarshal.GetValueRefOrNullRef for Lookup-and-Update
|
||||
🟡 **DO** use `CollectionsMarshal.GetValueRefOrAddDefault` for dictionary update patterns | .NET 6+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
_counts.TryGetValue(key, out int count);
|
||||
_counts[key] = count + 1;
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
ref int count = ref CollectionsMarshal.GetValueRefOrAddDefault(_counts, key, out _);
|
||||
count++;
|
||||
```
|
||||
|
||||
**Impact: ~48% faster for lookup-and-update patterns (95µs → 49µs).**
|
||||
|
||||
### Use Collection Expressions [] for Zero-Allocation Span Creation
|
||||
🟡 **DO** use collection expressions for `Span<T>` targets | C# 12 / .NET 8+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
int[] values = new int[] { a, b, c, d };
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
Span<int> values = [a, b, c, d];
|
||||
ReadOnlySpan<int> daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||||
```
|
||||
|
||||
**Impact: Zero heap allocation for span-targeted collection expressions.**
|
||||
|
||||
### Use EnsureCapacity on List/Stack/Queue Before Bulk Adds
|
||||
🟡 **DO** call `EnsureCapacity` before bulk insertions | .NET 6+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
var list = new List<int>();
|
||||
for (int i = 0; i < 10000; i++)
|
||||
list.Add(i);
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
var list = new List<int>();
|
||||
list.EnsureCapacity(10000);
|
||||
for (int i = 0; i < 10000; i++)
|
||||
list.Add(i);
|
||||
```
|
||||
|
||||
**Impact: Reduces reallocations and array copies during bulk operations.**
|
||||
|
||||
### Use TryGetNonEnumeratedCount for Pre-Sizing
|
||||
🟡 **DO** use `TryGetNonEnumeratedCount` to pre-size destination collections | .NET 6+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
var results = new List<int>();
|
||||
foreach (var item in source)
|
||||
results.Add(Transform(item));
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
var results = source.TryGetNonEnumeratedCount(out int count)
|
||||
? new List<int>(count)
|
||||
: new List<int>();
|
||||
foreach (var item in source)
|
||||
results.Add(Transform(item));
|
||||
```
|
||||
|
||||
**Impact: Avoids O(n) enumeration for counting; eliminates resizing allocations.**
|
||||
|
||||
### Hoist Static Data Out of Method Bodies
|
||||
🟡 **AVOID** creating collections with static/deterministic data inside method bodies | .NET Core+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
public string Convert(long number)
|
||||
{
|
||||
var groupsMap = new Dictionary<long, Func<long, string>>
|
||||
{
|
||||
{ 1_000_000_000, n => $"{Convert(n)} billion" },
|
||||
{ 1_000_000, n => $"{Convert(n)} million" },
|
||||
{ 1_000, n => $"{Convert(n)} thousand" },
|
||||
};
|
||||
}
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
private static readonly FrozenDictionary<long, Func<long, string>> s_groupsMap =
|
||||
new Dictionary<long, Func<long, string>>
|
||||
{
|
||||
{ 1_000_000_000, n => $"{Convert(n)} billion" },
|
||||
{ 1_000_000, n => $"{Convert(n)} million" },
|
||||
{ 1_000, n => $"{Convert(n)} thousand" },
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
public string Convert(long number)
|
||||
{
|
||||
// ... use s_groupsMap
|
||||
}
|
||||
```
|
||||
|
||||
**Impact: Eliminates collection + internal storage + closure allocations per call. For a Dictionary with N entries, saves ~N+3 allocations per invocation.**
|
||||
|
||||
### Add Overloads to Avoid params Array Allocation
|
||||
🟡 **DO** add 1- and 2-argument overloads for methods that accept `params T[]`. On .NET 10 also expose a `params ReadOnlySpan<T>` overload | works on .NET 8 and .NET 10
|
||||
|
||||
❌ (single `params T[]` overload allocates a new array on every call, including the common 1-argument case)
|
||||
```csharp
|
||||
public static string Transform(this string input, params IStringTransformer[] transformers) =>
|
||||
transformers.Aggregate(input, (current, t) => t.Transform(current));
|
||||
|
||||
"hello".Transform(To.TitleCase);
|
||||
```
|
||||
✅ Option A — explicit overloads for common arities (works on .NET 8 and .NET 10)
|
||||
```csharp
|
||||
public static string Transform(this string input, IStringTransformer transformer) =>
|
||||
transformer.Transform(input);
|
||||
|
||||
public static string Transform(this string input, IStringTransformer t1, IStringTransformer t2) =>
|
||||
t2.Transform(t1.Transform(input));
|
||||
|
||||
public static string Transform(this string input, params IStringTransformer[] transformers) =>
|
||||
transformers.Aggregate(input, (current, t) => t.Transform(current));
|
||||
```
|
||||
✅ Option B — `.NET 10 / C# 14` adds a span overload (eliminates the allocation for all arities)
|
||||
```csharp
|
||||
public static string Transform(this string input, params ReadOnlySpan<IStringTransformer> transformers)
|
||||
{
|
||||
foreach (var t in transformers)
|
||||
input = t.Transform(input);
|
||||
return input;
|
||||
}
|
||||
```
|
||||
⚠️ Option B does **not** compile on `net8.0`: `params ReadOnlySpan<T>` requires C# 13 (default on .NET 9+). On .NET 8 ship only Option A.
|
||||
|
||||
**Impact: Option A eliminates the array allocation for 1- and 2-argument calls on every target. Option B eliminates it for all arities on .NET 10.**
|
||||
|
||||
## Detection
|
||||
|
||||
Scan recipes for collection and LINQ anti-patterns. Run these and report exact counts.
|
||||
|
||||
```bash
|
||||
# Static Dictionary not using FrozenDictionary (read-only after init)
|
||||
grep -rn --include='*.cs' 'static readonly Dictionary<' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
|
||||
# Static FrozenDictionary (already optimized — verify the inverse)
|
||||
grep -rn --include='*.cs' 'static readonly FrozenDictionary<' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
|
||||
# Per-call List allocation (inside method bodies, not static/readonly fields)
|
||||
grep -rn --include='*.cs' 'new List<' --exclude-dir=bin --exclude-dir=obj . | grep -v 'static\|readonly' | wc -l
|
||||
|
||||
# Per-call Dictionary allocation (inside method bodies, not static/readonly fields)
|
||||
grep -rn --include='*.cs' 'new Dictionary<' --exclude-dir=bin --exclude-dir=obj . | grep -v 'static\|readonly' | wc -l
|
||||
|
||||
# StringComparer.CurrentCulture usage (almost always wrong in library code — use Ordinal)
|
||||
grep -rn --include='*.cs' 'StringComparer.CurrentCulture' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
|
||||
# LINQ chains in extension/hot-path files (.Select, .Where, .Cast, .Take, .Aggregate)
|
||||
grep -rn --include='*.cs' -E '\.(Select|Where|Cast|Take|Aggregate)\(' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
```
|
||||
|
||||
For the LINQ chain recipe: any hit in a file whose name ends in `Extensions.cs`, `Formatter.cs`, or implements a method called from a public extension method is a hot-path candidate. Inspect each hit in these files and flag LINQ chains that allocate delegates, enumerators, or intermediate collections on every call. Hits in localization converters or one-time initialization are lower priority.
|
||||
|
||||
### Patterns Requiring Manual Review
|
||||
|
||||
- **ContainsKey + indexer double-lookup**: Requires verifying the same key is used in a subsequent indexer access — multi-line/multi-statement context
|
||||
- **LINQ on hot paths**: The LINQ chain recipe above catches call sites, but distinguishing hot-path from cold-path requires context. Prioritize hits in `*Extensions.cs` and `*Formatter.cs` files, which are typically called on every user invocation
|
||||
- **`new Dictionary/List<` in method bodies vs fields**: The grep heuristic (`grep -v 'static\|readonly'`) catches most cases but may include false positives from field initializers without `static`/`readonly` — spot-check flagged lines
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
# Critical .NET Performance Anti-Patterns
|
||||
|
||||
17 patterns that cause deadlocks, order-of-magnitude regressions, or excessive allocations.
|
||||
|
||||
## Async / Tasks
|
||||
|
||||
### Never Block on Async (Sync-over-Async)
|
||||
🔴 **AVOID** | .NET Core+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
public string GetData()
|
||||
=> GetDataAsync().Result;
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
public async Task<string> GetDataAsync()
|
||||
=> await GetDataInternalAsync();
|
||||
```
|
||||
**Impact: Deadlocks or thread pool starvation; wastes threads, destroys scalability.**
|
||||
|
||||
### Never Await a ValueTask Multiple Times
|
||||
🔴 **AVOID** | .NET Core 2.1+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
ValueTask<int> vt = SomeMethodAsync();
|
||||
int a = await vt;
|
||||
int b = await vt;
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
int result = await SomeMethodAsync();
|
||||
```
|
||||
**Impact: Undefined behavior — silent data corruption or exceptions.**
|
||||
|
||||
## Memory / Allocation
|
||||
|
||||
### Use Span\<T\> / AsSpan Instead of Substring for Slicing
|
||||
🔴 **DO** | .NET Core 2.1+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
string sub = input.Substring(5, 10);
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
ReadOnlySpan<char> sub = input.AsSpan(5, 10);
|
||||
```
|
||||
**Impact: Eliminates per-slice allocations; 2-4x faster via vectorization.**
|
||||
|
||||
### Use ArrayPool\<T\> for Temporary Buffers
|
||||
🔴 **DO** | .NET Core+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
byte[] buf = new byte[4096];
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
byte[] buf = ArrayPool<byte>.Shared.Rent(4096);
|
||||
Process(buf);
|
||||
ArrayPool<byte>.Shared.Return(buf);
|
||||
```
|
||||
**Impact: Dramatically reduces GC pressure for buffer-heavy workloads.**
|
||||
|
||||
### Avoid stackalloc in Loops
|
||||
🔴 **AVOID** | .NET 5+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
for (int i = 0; i < 10_000; i++)
|
||||
Span<byte> buf = stackalloc byte[1024];
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
Span<byte> buf = stackalloc byte[1024];
|
||||
for (int i = 0; i < 10_000; i++) { Process(buf); }
|
||||
```
|
||||
**Impact: StackOverflowException — unrecoverable, no catch possible.**
|
||||
|
||||
### Avoid Boxing Value Types
|
||||
🔴 **AVOID** | .NET 6+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
string s = string.Format("{0}.{1}", major, minor);
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
string s = $"{major}.{minor}";
|
||||
```
|
||||
**Impact: When replacing `string.Format` with C# 10+ interpolation, typical improvements are ~40% faster with significantly less allocation. Actual gains vary by call site.**
|
||||
|
||||
## Strings
|
||||
|
||||
### Use StringComparison.Ordinal for Non-Linguistic Comparisons
|
||||
🔴 **DO** | .NET Core+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
bool found = text.IndexOf("Content-Type") >= 0;
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
bool found = text.Contains("Content-Type", StringComparison.Ordinal);
|
||||
```
|
||||
**Impact: 2-3x faster; OrdinalIgnoreCase hash codes ~3.3x faster.**
|
||||
|
||||
### Use AsSpan Instead of Substring
|
||||
🔴 **DO** | .NET Core 2.1+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
int val = int.Parse(str.Substring(5, 3));
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
int val = int.Parse(str.AsSpan(5, 3));
|
||||
```
|
||||
**Impact: Eliminates one string allocation per parse operation.**
|
||||
|
||||
## Regular Expressions
|
||||
|
||||
### Use Source-Generated Regex [GeneratedRegex]
|
||||
🔴 **ALWAYS** use `[GeneratedRegex]` for all static regex patterns | .NET 7+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
private static readonly Regex s_re =
|
||||
new(@"\w+@\w+\.\w+", RegexOptions.Compiled);
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
[GeneratedRegex(@"\w+@\w+\.\w+")]
|
||||
private static partial Regex EmailRegex();
|
||||
```
|
||||
**Impact: Always beneficial or neutral for static patterns — near-zero startup, better throughput, and required for AOT/trimming scenarios.**
|
||||
|
||||
### Avoid Nested Quantifiers (Catastrophic Backtracking)
|
||||
🔴 **AVOID** | .NET Core+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
var r = new Regex(@"^(\w+)+$");
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
var r = new Regex(@"^\w+$", RegexOptions.NonBacktracking);
|
||||
```
|
||||
**Impact: Can hang process indefinitely on crafted input.**
|
||||
|
||||
### Use TryGetValue Instead of ContainsKey + Indexer
|
||||
🔴 **DO** | .NET Core+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
if (dict.ContainsKey(key))
|
||||
Use(dict[key]);
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
if (dict.TryGetValue(key, out var value))
|
||||
Use(value);
|
||||
```
|
||||
**Impact: ~2x faster (50% reduction in lookup time).**
|
||||
|
||||
### Avoid LINQ in Hot Paths
|
||||
🔴 **AVOID** | .NET Core+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
bool found = items.Any(x => x.Name == target);
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
bool found = false;
|
||||
foreach (var item in items)
|
||||
if (item.Name == target) { found = true; break; }
|
||||
```
|
||||
**Impact: Eliminates 1-3 allocations per call; measurable in tight loops.**
|
||||
|
||||
### Don't Iterate IEnumerable Multiple Times
|
||||
🔴 **AVOID** | .NET Core+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
foreach (Type t in types) { Validate(t); }
|
||||
_types = types.ToArray();
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
Type[] arr = types.ToArray();
|
||||
foreach (Type t in arr) { Validate(t); }
|
||||
_types = arr;
|
||||
```
|
||||
**Impact: Halves enumeration cost; prevents bugs from re-executing deferred queries.**
|
||||
|
||||
## JSON Serialization
|
||||
|
||||
### Use System.Text.Json Source Generator
|
||||
🔴 **DO** | .NET 6+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
string json = JsonSerializer.Serialize(post);
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
[JsonSerializable(typeof(BlogPost))]
|
||||
internal partial class AppJsonCtx : JsonSerializerContext { }
|
||||
string json = JsonSerializer.Serialize(post, AppJsonCtx.Default.BlogPost);
|
||||
```
|
||||
**Impact: 37-44% faster; enables trimming and Native AOT.**
|
||||
|
||||
### Cache JsonSerializerOptions
|
||||
🔴 **DO** | .NET 5+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
JsonSerializer.Serialize(obj, new JsonSerializerOptions());
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
private static readonly JsonSerializerOptions s_opts = new();
|
||||
JsonSerializer.Serialize(obj, s_opts);
|
||||
```
|
||||
**Impact: Up to 592x slower without caching (.NET 6); always cache or use defaults.**
|
||||
|
||||
## Networking
|
||||
|
||||
### Reuse HttpClient Instances
|
||||
🔴 **DO** | .NET Core 2.1+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
using var client = new HttpClient();
|
||||
await client.GetStringAsync(url);
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
private static readonly HttpClient s_http = new(new SocketsHttpHandler
|
||||
{ PooledConnectionLifetime = TimeSpan.FromMinutes(5) });
|
||||
await s_http.GetStringAsync(url);
|
||||
```
|
||||
**Impact: Prevents socket exhaustion; 6-12x faster concurrent HTTPS.**
|
||||
|
||||
## General
|
||||
|
||||
### Use SearchValues\<T\> for Repeated Set Searches
|
||||
🔴 **DO** | .NET 8+ (works on both targets; .NET 10 adds multi-string overloads)
|
||||
|
||||
❌
|
||||
```csharp
|
||||
int pos = text.IndexOfAny("ABCDEF".ToCharArray());
|
||||
```
|
||||
✅ (.NET 8 and .NET 10 — `SearchValues<char>` is the same API on both)
|
||||
```csharp
|
||||
private static readonly SearchValues<char> s_hex = SearchValues.Create("ABCDEF");
|
||||
int pos = text.AsSpan().IndexOfAny(s_hex);
|
||||
```
|
||||
✅ (.NET 10 only — multi-string `SearchValues<string>`)
|
||||
```csharp
|
||||
private static readonly SearchValues<string> s_keywords =
|
||||
SearchValues.Create(["error", "warning", "fatal"], StringComparison.OrdinalIgnoreCase);
|
||||
int pos = log.AsSpan().IndexOfAny(s_keywords); // SearchValues<string> overload is .NET 9+ BCL
|
||||
```
|
||||
On `net8.0` use a `SearchValues<char>` with the first letter of each keyword and then fall back to `string.IndexOf(StringComparison.Ordinal)`.
|
||||
|
||||
**Impact: 2-10x faster for chars (both targets); 10-30x faster for multi-string on .NET 10.**
|
||||
|
||||
## Detection
|
||||
|
||||
Scan recipes for critical anti-patterns. Run these and report exact counts of issues found in each case.
|
||||
|
||||
```bash
|
||||
# .IndexOf(string) without StringComparison (culture-aware, 2-3x slower)
|
||||
grep -rn --include='*.cs' -E '\.IndexOf\("[^"]+"\)' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
|
||||
# .Substring( calls (allocates new string — consider AsSpan)
|
||||
grep -rn --include='*.cs' '\.Substring(' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
|
||||
# .StartsWith/.EndsWith without StringComparison (culture-aware, 2-3x slower)
|
||||
grep -rn --include='*.cs' -E '\.(StartsWith|EndsWith)\("[^"]+"\)' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
|
||||
# .Contains(string) without StringComparison — NOTE: will also match collection .Contains() calls; filter to string receivers
|
||||
grep -rn --include='*.cs' -E '\.Contains\("[^"]+"\)' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
```
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
# Anti-Pattern Grep Library
|
||||
|
||||
Consolidated grep patterns for automated scanning. Run these with the Grep tool using `type: "cs"` filter.
|
||||
|
||||
All patterns use ripgrep regex syntax. Run during Phase 1 (Discovery) broad scan.
|
||||
|
||||
> **See also:** each topic catalog ([critical-patterns.md](critical-patterns.md), [async-patterns.md](async-patterns.md), [memory-and-strings.md](memory-and-strings.md), [collections-and-linq.md](collections-and-linq.md), [regex-patterns.md](regex-patterns.md), [io-and-serialization.md](io-and-serialization.md), [structural-patterns.md](structural-patterns.md)) has its own Detection section with topic-specific recipes and ratio-counting guidance. This file is the consolidated cross-cutting library; load topic files when their signals are present.
|
||||
|
||||
---
|
||||
|
||||
## ASYNC anti-patterns (CRITICAL)
|
||||
|
||||
```
|
||||
\.Result\b
|
||||
\.Wait\(\)
|
||||
\.GetAwaiter\(\)\.GetResult\(\)
|
||||
async void\b
|
||||
Task\.Run\(
|
||||
\.WriteAsync\([^,]*\)
|
||||
```
|
||||
|
||||
**False positive note**: `.Result` matches `Task.FromResult` -- verify actual blocking before flagging.
|
||||
|
||||
---
|
||||
|
||||
## Memory anti-patterns (MEM)
|
||||
|
||||
```
|
||||
new byte\[\d{4,}\]
|
||||
new byte\[
|
||||
new MemoryStream\(\)
|
||||
\.Substring\(
|
||||
new StringBuilder\(\)
|
||||
new List<.*>\(\)
|
||||
new Dictionary<.*>\(\)
|
||||
_logger\.Log(Debug|Trace|Information|Warning|Error|Critical)\(\$"
|
||||
\.Split\(
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LINQ anti-patterns
|
||||
|
||||
```
|
||||
\.Count\(\)\s*[><=!]
|
||||
\.ToList\(\)\.Where\(
|
||||
\.ToList\(\)\.Select\(
|
||||
\.Select\(.*\)\.Where\(
|
||||
\.OrderBy.*\.Where\(
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database / CosmosDB anti-patterns (DB)
|
||||
|
||||
```
|
||||
\.Include\(.*\.Include\(
|
||||
await.*foreach.*await.*Async
|
||||
ReadItemAsync
|
||||
GetItemQueryIterator
|
||||
GetItemLinqQueryable
|
||||
\.RequestCharge
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## JSON anti-patterns
|
||||
|
||||
```
|
||||
new JsonSerializerOptions
|
||||
new JsonSerializerSettings
|
||||
JsonConvert\.Serialize
|
||||
JsonConvert\.Deserialize
|
||||
new JsonSerializer
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Caching anti-patterns (CACHE)
|
||||
|
||||
```
|
||||
GetAsync\(
|
||||
SendAsync\(
|
||||
_cache\.TryGetValue
|
||||
DistributedCache
|
||||
AddMemoryCache\(\)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HttpClient misuse (HTTP)
|
||||
|
||||
```
|
||||
new HttpClient\(
|
||||
new HttpClient\b
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exception control flow (EXC)
|
||||
|
||||
```
|
||||
catch\s*\(Exception\b
|
||||
catch\s*\(KeyNotFoundException
|
||||
catch\s*\(FormatException
|
||||
catch\s*\(InvalidOperationException
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## String anti-patterns (STR)
|
||||
|
||||
```
|
||||
\+= "
|
||||
\+= \$"
|
||||
\.ToLower\(\)
|
||||
\.ToUpper\(\)
|
||||
String\.Format\(
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Concurrency anti-patterns (CONC)
|
||||
|
||||
```
|
||||
lock\s*\(
|
||||
new SemaphoreSlim
|
||||
HttpContext.*Task\.Run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Startup & Pipeline (STARTUP)
|
||||
|
||||
```
|
||||
UseResponseCompression
|
||||
AddResponseCompression
|
||||
ShortCircuit
|
||||
AddOutputCache
|
||||
UseOutputCache
|
||||
TieredPGO
|
||||
PublishReadyToRun
|
||||
ApplicationStarted
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Metrics & Observability (METRICS)
|
||||
|
||||
```
|
||||
new Meter\(
|
||||
CreateCounter
|
||||
CreateHistogram
|
||||
AddMeter
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CancellationToken coverage
|
||||
|
||||
```
|
||||
async Task[<\s].*\)\s*$
|
||||
```
|
||||
|
||||
This pattern finds async methods whose signature ends without a CancellationToken parameter. Verify each match -- some may be interface implementations where the token is propagated differently.
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
# I/O, Serialization & General Patterns
|
||||
|
||||
### Use HttpCompletionOption.ResponseHeadersRead for Streaming
|
||||
🟡 **DO** use `ResponseHeadersRead` when downloading large responses | .NET Core 3.0+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
var response = await client.GetAsync(uri);
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
using var response = await client.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead);
|
||||
using var stream = await response.Content.ReadAsStreamAsync();
|
||||
await stream.CopyToAsync(destinationStream);
|
||||
```
|
||||
|
||||
**Impact: ~2x faster for large downloads (10MB+), dramatically reduced memory usage.**
|
||||
|
||||
### Use Async FileStream Operations
|
||||
🟡 **DO** use `FileStream` with `useAsync: true` for scalable file I/O | .NET 6+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
using var fs = new FileStream(path, FileMode.Open);
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
await using var fs = new FileStream(path, FileMode.Open, FileAccess.Read,
|
||||
FileShare.Read, bufferSize: 4096, useAsync: true);
|
||||
|
||||
byte[] buffer = new byte[1024];
|
||||
while (await fs.ReadAsync(buffer) != 0) { /* process */ }
|
||||
```
|
||||
|
||||
**Impact: Up to 3x faster async reads; allocation reduced from megabytes to hundreds of bytes.**
|
||||
|
||||
### Use Memory\<byte\> Overloads for Stream.ReadAsync/WriteAsync
|
||||
🟡 **DO** use `Memory<byte>`-based stream overloads instead of `byte[]` overloads | .NET 5+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
await stream.ReadAsync(buffer, 0, buffer.Length);
|
||||
await stream.WriteAsync(buffer, 0, buffer.Length);
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
await stream.ReadAsync(buffer.AsMemory());
|
||||
await stream.WriteAsync(buffer.AsMemory());
|
||||
```
|
||||
|
||||
**Impact: Eliminates ~72 KB allocation per 1,000 read/write pairs on NetworkStream.**
|
||||
|
||||
### Use Span-Based TryFormat for Number Formatting
|
||||
🟡 **DO** use `TryFormat` to format numbers into `Span<char>` buffers | .NET Core 2.1+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
string formatted = value.ToString();
|
||||
destination.Write(formatted);
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
Span<char> buffer = stackalloc char[20];
|
||||
if (value.TryFormat(buffer, out int charsWritten))
|
||||
destination.Write(buffer[..charsWritten]);
|
||||
```
|
||||
|
||||
**Impact: Int32.ToString() ~2x faster in .NET Core 2.1, Int32 parsing ~5x faster in .NET Core 3.0.**
|
||||
|
||||
### Use static readonly for Runtime Devirtualization
|
||||
🟡 **DO** store implementations in `static readonly` fields for JIT devirtualization | .NET Core 3.0+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
private static Base s_impl = new DerivedImpl();
|
||||
s_impl.Process();
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
private static readonly Base s_impl = new DerivedImpl();
|
||||
s_impl.Process();
|
||||
|
||||
private static readonly bool s_feature =
|
||||
Environment.GetEnvironmentVariable("Feature") == "1";
|
||||
```
|
||||
|
||||
**Impact: Virtual call eliminated entirely — can be inlined to zero overhead. Dead code elimination in tier 1.**
|
||||
|
||||
### Avoid Explicit Static Constructors — Use Field Initializers
|
||||
🟡 **AVOID** explicit `static` constructors when field initializers suffice | .NET Core 3.0+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
class Foo
|
||||
{
|
||||
static readonly int s_value;
|
||||
static Foo() { s_value = ComputeValue(); }
|
||||
}
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
class Foo
|
||||
{
|
||||
static readonly int s_value = ComputeValue();
|
||||
}
|
||||
```
|
||||
|
||||
**Impact: Enables better JIT optimization and reduces potential lock overhead on static method access.**
|
||||
|
||||
## Detection
|
||||
|
||||
Scan recipes for I/O and serialization anti-patterns. Run these and report exact counts.
|
||||
|
||||
```bash
|
||||
# new HttpClient() (socket exhaustion risk)
|
||||
grep -rn --include='*.cs' 'new HttpClient(' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
|
||||
# new JsonSerializerOptions() not cached (592x slower in .NET 6)
|
||||
grep -rn --include='*.cs' 'new JsonSerializerOptions' --exclude-dir=bin --exclude-dir=obj . | grep -v 'static\|readonly' | wc -l
|
||||
```
|
||||
|
||||
### Patterns Requiring Manual Review
|
||||
|
||||
- **`JsonSerializer.Serialize/Deserialize` without source-gen context**: Can't determine from grep if a context parameter is passed
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
---
|
||||
description: >-
|
||||
Performance measurement guide for dotnet-performance skill. Covers tool
|
||||
selection per category, KPI targets, BenchmarkDotNet, k6 load testing,
|
||||
CI/CD integration, and live process CLI commands.
|
||||
metadata:
|
||||
tags: [measurement, benchmarkdotnet, k6, dotnet-counters, kpi]
|
||||
---
|
||||
|
||||
# Measurement Guide
|
||||
|
||||
How to measure performance before and after applying optimizations. Never optimize without baseline data.
|
||||
|
||||
---
|
||||
|
||||
## Tool Selection Decision Table
|
||||
|
||||
Map each optimization category to the appropriate measurement tools:
|
||||
|
||||
| Category | Primary Tool | Secondary Tool | What to Measure |
|
||||
|---|---|---|---|
|
||||
| MEM | `dotnet-counters` (gc-heap-size, alloc-rate) | `dotnet-gcdump` comparison | Allocation rate reduction, GC collection frequency |
|
||||
| ASYNC | `dotnet-counters` (threadpool-queue-length, thread-count) | App Insights dependency tracking | Thread pool starvation, blocked threads |
|
||||
| LINQ | BenchmarkDotNet `[MemoryDiagnoser]` | `dotnet-trace` hot path | Allocation per operation, throughput |
|
||||
| DB | `response.RequestCharge` logging | App Insights DB dependency | RU cost per operation, query latency |
|
||||
| JSON | BenchmarkDotNet serialization benchmark | `dotnet-counters` alloc-rate | Throughput (ops/sec), bytes allocated |
|
||||
| CACHE | App Insights dependency duration | Custom hit ratio counter | Cache hit rate, dependency call reduction |
|
||||
| DI | `dotnet-counters` alloc-rate | Load test comparison | Object creation overhead |
|
||||
| CONC | `dotnet-counters` (monitor-lock-contention-count) | `dotnet-trace` contention events | Lock wait time, throughput under load |
|
||||
| HTTP | `dotnet-counters` Microsoft.AspNetCore.Hosting | k6/NBomber load test | Request duration, throughput |
|
||||
| EXC | `dotnet-counters` exception-count | App Insights exceptions | Exception rate per interval |
|
||||
| RESP | Network tab / curl with timing | k6 response size check | Response size (bytes), transfer time |
|
||||
| STR | BenchmarkDotNet `[MemoryDiagnoser]` | `dotnet-counters` alloc-rate | String allocations per operation |
|
||||
| STARTUP | Startup time measurement | `dotnet-trace` startup events | Time to first request, cold start latency |
|
||||
| METRICS | `MetricCollector<T>` in tests | Prometheus/Grafana dashboard | Metric emission, cardinality |
|
||||
|
||||
---
|
||||
|
||||
## KPI Targets
|
||||
|
||||
Standard targets for ASP.NET Core APIs. Use as thresholds when evaluating optimization impact:
|
||||
|
||||
| Metric | Target | Red Flag |
|
||||
|---|---|---|
|
||||
| P50 response time | < 100ms | > 200ms |
|
||||
| P95 response time | < 500ms | > 1000ms |
|
||||
| P99 response time | < 1000ms | > 2000ms |
|
||||
| Error rate (5xx) | < 0.1% | > 1% |
|
||||
| CPU utilization | < 70% sustained | > 85% |
|
||||
| Memory working set | < 80% | > 90% |
|
||||
| Thread pool queue length | < 10 sustained | > 50 |
|
||||
| GC time percentage | < 10% | > 20% |
|
||||
| Allocation rate | Trend down after optimization | Sustained increase |
|
||||
|
||||
---
|
||||
|
||||
## BenchmarkDotNet Guidance
|
||||
|
||||
Use for micro-optimizations on hot paths (MEM, LINQ, JSON, STR categories).
|
||||
|
||||
**When to benchmark**: Hot-path changes where the difference is in nanoseconds or bytes allocated. Not needed for architectural changes (caching, DI lifetime) — use load testing instead.
|
||||
|
||||
**Minimum setup**:
|
||||
```csharp
|
||||
[MemoryDiagnoser]
|
||||
[SimpleJob(RuntimeMoniker.Net90)]
|
||||
public class MyBenchmark
|
||||
{
|
||||
[Benchmark(Baseline = true)]
|
||||
public void Original() { /* original code */ }
|
||||
|
||||
[Benchmark]
|
||||
public void Optimized() { /* optimized code */ }
|
||||
}
|
||||
```
|
||||
|
||||
**Run command**: `dotnet run -c Release --project path/to/benchmark`
|
||||
|
||||
**Common pitfalls**:
|
||||
- Running in Debug mode (JIT optimizations disabled, results meaningless)
|
||||
- Not returning computed values (JIT eliminates dead code)
|
||||
- Ignoring allocation metrics (throughput may improve but allocations increase)
|
||||
- Benchmarking with a debugger attached
|
||||
- Including setup costs in the measured method
|
||||
|
||||
---
|
||||
|
||||
## Load Testing
|
||||
|
||||
For HIGH-impact optimizations, perform before/after load testing to validate real-world improvement.
|
||||
|
||||
**k6 template**:
|
||||
```javascript
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
|
||||
export const options = {
|
||||
stages: [
|
||||
{ duration: '30s', target: 20 },
|
||||
{ duration: '1m', target: 20 },
|
||||
{ duration: '10s', target: 0 },
|
||||
],
|
||||
thresholds: {
|
||||
http_req_duration: ['p(50)<100', 'p(95)<500', 'p(99)<1000'],
|
||||
http_req_failed: ['rate<0.01'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function () {
|
||||
const res = http.get('http://localhost:5000/your-endpoint');
|
||||
check(res, {
|
||||
'status is 200': (r) => r.status === 200,
|
||||
'p95 under 500ms': (r) => r.timings.duration < 500,
|
||||
});
|
||||
sleep(1);
|
||||
}
|
||||
```
|
||||
|
||||
**While load testing, monitor simultaneously**:
|
||||
```bash
|
||||
dotnet-counters monitor -n <ProcessName> --counters System.Runtime,Microsoft.AspNetCore.Hosting
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CI/CD Integration
|
||||
|
||||
For PR regression detection, use `benchmark-action/github-action-benchmark`:
|
||||
|
||||
```yaml
|
||||
- uses: benchmark-action/github-action-benchmark@v1
|
||||
with:
|
||||
tool: 'benchmarkdotnet'
|
||||
output-file-path: BenchmarkDotNet.Artifacts/results/*.json
|
||||
alert-threshold: '150%'
|
||||
comment-on-alert: true
|
||||
fail-on-alert: true
|
||||
```
|
||||
|
||||
This fails the PR if any benchmark regresses by more than 50% compared to the baseline.
|
||||
|
||||
---
|
||||
|
||||
## Code Review Mode: Quick Reference Commands
|
||||
|
||||
```bash
|
||||
# Baseline runtime health
|
||||
dotnet-counters monitor -n <ProcessName> --counters System.Runtime
|
||||
|
||||
# ASP.NET Core request metrics
|
||||
dotnet-counters monitor -n <ProcessName> --counters Microsoft.AspNetCore.Hosting
|
||||
|
||||
# Full monitoring (runtime + HTTP + custom meters)
|
||||
dotnet-counters monitor -n <ProcessName> --counters System.Runtime,Microsoft.AspNetCore.Hosting,Microsoft.AspNetCore.Server.Kestrel
|
||||
|
||||
# GC heap snapshot for before/after comparison
|
||||
dotnet-gcdump collect -n <ProcessName> -o before.gcdump
|
||||
# ... apply optimization ...
|
||||
dotnet-gcdump collect -n <ProcessName> -o after.gcdump
|
||||
|
||||
# 30-second CPU trace
|
||||
dotnet-trace collect -n <ProcessName> --duration 00:00:30
|
||||
dotnet-trace convert trace.nettrace --format speedscope
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagnostic Mode: Full CLI Commands
|
||||
|
||||
When profiling a live process (Mode A), use these commands by investigation stage:
|
||||
|
||||
```bash
|
||||
# Stage 1: Live triage
|
||||
dotnet-counters monitor -p <PID> --counters System.Runtime
|
||||
dotnet-counters monitor -n <ProcessName> --counters System.Runtime,Microsoft.AspNetCore.Hosting,Microsoft.AspNetCore.Server.Kestrel
|
||||
|
||||
# Stage 2: Stuck/hung process — get stacks immediately
|
||||
dotnet-stack report -p <PID>
|
||||
|
||||
# Stage 3: CPU + allocation hot paths
|
||||
dotnet-trace collect -p <PID> --duration 00:00:30
|
||||
dotnet-trace report <trace.nettrace> topN
|
||||
|
||||
# Stage 4: Heap composition
|
||||
dotnet-gcdump collect -p <PID> -o before.gcdump
|
||||
# ... apply optimization ...
|
||||
dotnet-gcdump collect -p <PID> -o after.gcdump
|
||||
dotnet-gcdump report <file.gcdump>
|
||||
|
||||
# Stage 5: Full dump for SOS analysis
|
||||
dotnet-dump collect -p <PID> --type Heap
|
||||
dotnet-dump analyze <dump> -c "dumpheap -stat" -c "exit"
|
||||
```
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
# Memory & String Patterns
|
||||
|
||||
### Use ReadOnlySpan\<byte\> for Constant Byte Data
|
||||
🟡 **DO** assign constant byte arrays to `ReadOnlySpan<byte>` | .NET 5+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
byte[] data = new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F };
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
ReadOnlySpan<byte> data = [0x48, 0x65, 0x6C, 0x6C, 0x6F];
|
||||
ReadOnlySpan<int> primes = [2, 3, 5, 7, 11, 13];
|
||||
```
|
||||
|
||||
**Impact: ~100x faster access than static byte[] field, zero allocation.**
|
||||
|
||||
### Use stackalloc for Small Temporary Buffers
|
||||
🟡 **DO** use `stackalloc` for small, fixed-size temporary buffers | .NET Core+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
char[] buffer = new char[64];
|
||||
guid.TryFormat(buffer, out int written);
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
Span<char> buffer = stackalloc char[64];
|
||||
guid.TryFormat(buffer, out int written);
|
||||
```
|
||||
|
||||
**Impact: Zero heap allocation, no GC pressure, instant alloc/dealloc.**
|
||||
|
||||
### Use Span.TryWrite for Allocation-Free Interpolation
|
||||
🟡 **DO** use `MemoryExtensions.TryWrite` to format into `Span<char>` buffers | .NET 6+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
string formatted = $"Date: {dt:R}";
|
||||
destination.Write(formatted);
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
Span<char> buffer = stackalloc char[64];
|
||||
buffer.TryWrite($"Date: {dt:R}", out int charsWritten);
|
||||
```
|
||||
|
||||
**Impact: Zero heap allocation for formatting operations.**
|
||||
|
||||
### Use Span.Split() for Zero-Allocation Splitting
|
||||
🟡 **DO** use `MemoryExtensions.Split` for allocation-free string splitting | **.NET 10 (or .NET 9) only — NOT available on .NET 8**
|
||||
|
||||
❌ (allocates `string[]` — the only built-in option on .NET 8)
|
||||
```csharp
|
||||
string[] parts = input.Split(',');
|
||||
```
|
||||
✅ .NET 10
|
||||
```csharp
|
||||
foreach (Range range in input.AsSpan().Split(','))
|
||||
{
|
||||
ReadOnlySpan<char> segment = input.AsSpan(range);
|
||||
}
|
||||
```
|
||||
✅ .NET 8 fallback — manual `IndexOf` loop on the span (no allocation)
|
||||
```csharp
|
||||
ReadOnlySpan<char> remaining = input.AsSpan();
|
||||
while (!remaining.IsEmpty)
|
||||
{
|
||||
int idx = remaining.IndexOf(',');
|
||||
ReadOnlySpan<char> segment = idx < 0 ? remaining : remaining[..idx];
|
||||
// ... use segment ...
|
||||
remaining = idx < 0 ? default : remaining[(idx + 1)..];
|
||||
}
|
||||
```
|
||||
|
||||
**Impact: 208 bytes → 0 bytes per split, 2x faster on .NET 10. The manual .NET 8 loop is also zero-allocation but more verbose.**
|
||||
|
||||
### Use UTF8 String Literals (u8 suffix)
|
||||
🟡 **DO** use the `u8` suffix for compile-time UTF8 `ReadOnlySpan<byte>` | .NET 7+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
byte[] header = Encoding.UTF8.GetBytes("Content-Type");
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
ReadOnlySpan<byte> header = "Content-Type"u8;
|
||||
```
|
||||
|
||||
**Impact: 17ns → 0.006ns — eliminates runtime transcoding entirely.**
|
||||
|
||||
### Use ReadOnlySpan\<char\> Pattern Matching with switch
|
||||
🟡 **DO** use `switch` on `ReadOnlySpan<char>` for allocation-free string matching | C# 11+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
switch (attr.Value.Trim()) { case "preserve": /* ... */ break; }
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
switch (attr.Value.AsSpan().Trim())
|
||||
{
|
||||
case "preserve": return Preserve;
|
||||
case "default": return Default;
|
||||
}
|
||||
```
|
||||
|
||||
**Impact: Eliminates string allocation from Trim() in switch-based dispatch.**
|
||||
|
||||
### Use params ReadOnlySpan\<T\> to Eliminate Array Allocations
|
||||
🟡 **DO** add `params ReadOnlySpan<T>` overloads to library methods | **.NET 10 (or .NET 9) only — requires C# 13**
|
||||
|
||||
❌ (the only option on .NET 8 — accept the array allocation, or add explicit 1/2/3-argument overloads)
|
||||
```csharp
|
||||
public static void Log(params string[] messages) { /* ... */ }
|
||||
Log("Starting", "Processing", "Done");
|
||||
```
|
||||
✅ .NET 10
|
||||
```csharp
|
||||
public static void Log(params ReadOnlySpan<string> messages) { /* ... */ }
|
||||
Log("Starting", "Processing", "Done");
|
||||
```
|
||||
✅ .NET 8 fallback — keep `params string[]` and add fixed-arity overloads for the hot common cases
|
||||
```csharp
|
||||
public static void Log(string m) { /* ... */ }
|
||||
public static void Log(string m1, string m2) { /* ... */ }
|
||||
public static void Log(string m1, string m2, string m3) { /* ... */ }
|
||||
public static void Log(params string[] messages) { /* fallback for 4+ args */ }
|
||||
```
|
||||
|
||||
**Impact: Eliminates params array allocation on .NET 10. On .NET 8 fixed-arity overloads cover the hot 1–3 argument cases.**
|
||||
|
||||
### Avoid Chained String-Returning Operations
|
||||
🟡 **AVOID** chains of 3+ string-returning method calls that each allocate intermediates | .NET Core+
|
||||
|
||||
**Pattern 1: Chained .Replace() calls**
|
||||
|
||||
❌
|
||||
```csharp
|
||||
string result = input.Replace("a", "b").Replace("c", "d").Replace("e", "f");
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
var sb = new StringBuilder(input.Length);
|
||||
// single pass replacing all patterns
|
||||
```
|
||||
|
||||
**Pattern 2: Chained Regex.Replace() calls**
|
||||
|
||||
❌
|
||||
```csharp
|
||||
public static string Underscore(this string input) =>
|
||||
Regex3.Replace(Regex2.Replace(Regex1.Replace(input, "$1_$2"), "$1_$2"), "_").ToLower();
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
return string.Create(totalLength, state, (span, s) => { /* write directly */ });
|
||||
```
|
||||
|
||||
**Pattern 3: += string concatenation in loops**
|
||||
|
||||
❌
|
||||
```csharp
|
||||
string result = "";
|
||||
foreach (var part in parts)
|
||||
result += separator + part;
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
var sb = new StringBuilder();
|
||||
foreach (var part in parts)
|
||||
sb.Append(separator).Append(part);
|
||||
return sb.ToString();
|
||||
```
|
||||
|
||||
**Impact: Eliminates N-1 intermediate string allocations per chain. For `+=` in loops, eliminates O(n²) total allocation.**
|
||||
|
||||
### Cache char.ToString() for Known Character Sets
|
||||
🟡 **DO** cache `char.ToString()` results when the set of characters is small and known | .NET Core+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
return symbol.ToString();
|
||||
|
||||
foreach (var prefix in UnitPrefixes)
|
||||
input = input.Replace(prefix.Value.Name, prefix.Key.ToString());
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
private static readonly FrozenDictionary<char, string> s_charStrings =
|
||||
new Dictionary<char, string>
|
||||
{
|
||||
['k'] = "k", ['M'] = "M", ['G'] = "G",
|
||||
}.ToFrozenDictionary();
|
||||
|
||||
return s_charStrings[symbol];
|
||||
```
|
||||
|
||||
**Impact: Eliminates one string allocation per char.ToString() call. Significant when called in loops or on hot paths.**
|
||||
|
||||
## Detection
|
||||
|
||||
Scan recipes for memory and string anti-patterns. Run these and report exact counts.
|
||||
|
||||
```bash
|
||||
# .ToLower()/.ToUpper() without culture parameter (allocates + culture-sensitive)
|
||||
grep -rn --include='*.cs' -E '\.(ToLower|ToUpper)\(\)' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
|
||||
# Chained .Replace( calls (3+ on one line — intermediate string allocations)
|
||||
grep -rn --include='*.cs' '\.Replace(.*\.Replace(.*\.Replace(' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
|
||||
# params in method signatures (array allocation per call)
|
||||
grep -rn --include='*.cs' 'params ' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
|
||||
# LINQ on strings — .All/.Any on IEnumerable<char> (replace with foreach loop)
|
||||
grep -rn --include='*.cs' -E '\.(All|Any)\(char\.' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
```
|
||||
|
||||
### Patterns Requiring Manual Review
|
||||
|
||||
- **Boxing via string.Format**: Can't determine argument types from grep — needs type analysis
|
||||
- **`+=` string concatenation in loops**: `+=` matches all types (int, list, event, string) — needs type context to confirm string
|
||||
- **`char.ToString()`**: Requires knowing the variable type is `char` — not reliably greppable
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
# Regex Patterns
|
||||
|
||||
### Choose the Right Regex Engine Mode
|
||||
🟡 **DO** use `[GeneratedRegex]` for all static regex patterns, but never remove `NonBacktracking` if present | .NET 7+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
var r = new Regex(dynamicPattern, RegexOptions.Compiled);
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
[GeneratedRegex("pattern")]
|
||||
private static partial Regex MyRegex();
|
||||
|
||||
var safe = new Regex(untrustedPattern, RegexOptions.NonBacktracking);
|
||||
|
||||
var oneOff = new Regex("pattern");
|
||||
```
|
||||
|
||||
**Impact: Source generator is always beneficial for static patterns. NonBacktracking prevents O(2^N) worst case — never remove it if present.**
|
||||
|
||||
### Use IsMatch When You Only Need a Boolean Result
|
||||
🟡 **DO** use `IsMatch` instead of `Match(...).Success` | .NET 7+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
bool found = Regex.Match(input, pattern).Success;
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
bool found = Regex.IsMatch(input, pattern);
|
||||
```
|
||||
|
||||
**Impact: Avoids Match object allocation; with NonBacktracking, ~3x faster by skipping capture computation.**
|
||||
|
||||
### Use Regex.Count/EnumerateMatches Instead of Matches
|
||||
🟡 **DO** use `Count()` and `EnumerateMatches()` for allocation-free match processing | .NET 7+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
int count = 0;
|
||||
Match m = regex.Match(text);
|
||||
while (m.Success) { count++; m = m.NextMatch(); }
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
int count = regex.Count(text);
|
||||
|
||||
foreach (ValueMatch m in Regex.EnumerateMatches(text, @"\b\w+\b"))
|
||||
{
|
||||
ReadOnlySpan<char> word = text.AsSpan(m.Index, m.Length);
|
||||
}
|
||||
```
|
||||
|
||||
**Impact: ~3x faster than Match/NextMatch with NonBacktracking. Zero allocations for both Count and EnumerateMatches.**
|
||||
|
||||
### Use Span-Based Regex APIs for Allocation-Free Matching
|
||||
🟡 **DO** use `ReadOnlySpan<char>` overloads for regex matching on spans | .NET 7+
|
||||
|
||||
❌
|
||||
```csharp
|
||||
string sub = largeBuffer.Substring(start, length);
|
||||
bool found = Regex.IsMatch(sub, pattern);
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
ReadOnlySpan<char> text = largeBuffer.AsSpan(start, length);
|
||||
foreach (ValueMatch m in Regex.EnumerateMatches(text, @"\b\w+\b"))
|
||||
{
|
||||
ReadOnlySpan<char> word = text.Slice(m.Index, m.Length);
|
||||
}
|
||||
```
|
||||
|
||||
**Impact: Eliminates string allocations when working with spans — particularly valuable in high-throughput parsing pipelines.**
|
||||
|
||||
## Detection
|
||||
|
||||
Scan recipes for regex anti-patterns. Run these and report exact counts.
|
||||
|
||||
```bash
|
||||
# Compiled regex count (startup cost budget — compare ratio to GeneratedRegex)
|
||||
grep -rn --include='*.cs' 'RegexOptions.Compiled' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
|
||||
# GeneratedRegex count (already optimized — verify the inverse)
|
||||
grep -rn --include='*.cs' 'GeneratedRegex' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
|
||||
# Uncached new Regex() calls (construction cost per call)
|
||||
grep -rn --include='*.cs' 'new Regex(' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
```
|
||||
|
||||
When `RegexOptions.Compiled` appears inside a class constructor or field initializer of an instantiated class (not a static singleton), count how many instances of that class are created at startup to determine total compiled regex budget. For example, if a `Rule` class compiles a regex in its constructor and 122 rules are registered, that is 122 compiled regexes at startup.
|
||||
|
||||
### Patterns Requiring Manual Review
|
||||
|
||||
- **`new Regex(` uncached**: Field assignment may span multiple lines — grep on one line is unreliable. Verify that matched instances are stored in `static readonly` fields or `[GeneratedRegex]`.
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# Structural Patterns
|
||||
|
||||
Patterns detected by the **absence** of a keyword or interface. These require codebase-wide counting scans, not single-file matching.
|
||||
|
||||
### Seal Classes for Devirtualization
|
||||
🟡 **DO** seal all leaf classes (those not subclassed) | .NET Core 3.0+
|
||||
|
||||
Sealing lets the JIT devirtualize/inline virtual calls and use pointer comparison for type checks. Every non-abstract, non-static class that is not subclassed should be sealed.
|
||||
|
||||
**Detection:** This is an absence pattern — scan for classes that are NOT sealed.
|
||||
|
||||
```bash
|
||||
# Count unsealed (non-abstract, non-static) classes
|
||||
grep -rn --include='*.cs' -E '^\s*((public|internal|private|protected|file)\s+)?(partial\s+)?class ' --exclude-dir=bin --exclude-dir=obj . | grep -v 'sealed' | grep -v 'abstract' | grep -v 'static' | wc -l
|
||||
|
||||
# Count already-sealed classes (verify the inverse)
|
||||
grep -rn --include='*.cs' 'sealed class' --exclude-dir=bin --exclude-dir=obj . | wc -l
|
||||
```
|
||||
|
||||
**Exclusions:** Do not seal classes that are subclassed elsewhere in the codebase. Identifying base classes requires manual review — grep for `: ClassName` patterns and cross-reference, but expect false positives from interface implementations and generic constraints.
|
||||
|
||||
❌
|
||||
```csharp
|
||||
internal class MyHandler : Base
|
||||
{ public override int Run() => 42; }
|
||||
```
|
||||
✅
|
||||
```csharp
|
||||
internal sealed class MyHandler : Base
|
||||
{ public override int Run() => 42; }
|
||||
```
|
||||
|
||||
**Impact: Virtual calls up to 500x faster; type checks ~25x faster. Severity scales with count.**
|
||||
|
||||
**Scale-based severity:**
|
||||
- 1-10 unsealed leaf classes → ℹ️ Info
|
||||
- 11-50 unsealed leaf classes → 🟡 Moderate
|
||||
- 50+ unsealed leaf classes → 🟡 Moderate (elevated priority)
|
||||
145
.skills/dotnet-security-review/SKILL.md
Normal file
145
.skills/dotnet-security-review/SKILL.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
---
|
||||
name: dotnet-security-review
|
||||
description: >-
|
||||
Performs a systematic C#/ASP.NET Core security code review on .NET 8 (C# 12)
|
||||
and .NET 10 (C# 14) codebases. Covers OWASP Top 10, authentication/authorization
|
||||
audit, input validation, cryptography, dependency vulnerabilities, security
|
||||
headers, middleware pipeline, and CI/CD security posture.
|
||||
metadata:
|
||||
platform: ".NET 8 and .NET 10 (no .NET 9 projects in scope)"
|
||||
---
|
||||
|
||||
# Security Code Review for C# / ASP.NET Core (.NET 8 + .NET 10)
|
||||
|
||||
You are a security auditor performing a thorough, evidence-based code review. Every finding MUST include file path, line number, severity, impact, and a concrete fix.
|
||||
|
||||
## Step 0 — Detect the target framework
|
||||
|
||||
Before scoring findings, follow `../../references/detect-target-framework.md`. The security guidance below applies to **both** .NET 8 and .NET 10 unless explicitly marked. A few items are .NET 10-only — when reviewing a .NET 8 project, don't recommend them as "fixes":
|
||||
|
||||
- **ASP.NET Core Identity passkeys** (`AddPasskeys()`) — .NET 10 only. On .NET 8, recommend external IdP / `Fido2NetLib` or password+TOTP.
|
||||
- **Minimal-API built-in validation** (`AddValidation()`) — .NET 10 only. On .NET 8, FluentValidation + `IEndpointFilter` is the safe equivalent.
|
||||
- **First-party `Microsoft.AspNetCore.OpenApi`** — .NET 9+ only. On .NET 8 the project should use `Swashbuckle.AspNetCore`; flag missing OpenAPI security schemes accordingly.
|
||||
- **`HybridCache`** — .NET 9+ only. On .NET 8 verify `IDistributedCache` configurations (encryption-at-rest, key prefixing, TLS to Redis) directly.
|
||||
- The C# 14 `field` keyword, `extension(...)` blocks, null-conditional assignment, and partial constructors **do not compile on net8.0** — never propose security fixes that introduce them on a .NET 8 project.
|
||||
|
||||
All cryptography, JWT, authorization-policy, header, and middleware guidance applies identically on both targets.
|
||||
|
||||
## Target Selection
|
||||
|
||||
The user's arguments are in `$ARGUMENTS`.
|
||||
|
||||
- If `$ARGUMENTS` contains a file path or directory, review that target.
|
||||
- If `$ARGUMENTS` is "all", review the entire codebase starting from the solution root.
|
||||
- If `$ARGUMENTS` is empty, run `git diff --name-only HEAD~5` to find recently changed `.cs` files. If none, ask the user what to review.
|
||||
|
||||
When reviewing a directory or "all", use Glob to find `**/*.cs` files, then prioritize:
|
||||
1. Controllers, filters, middleware (`*Controller.cs`, `*Filter.cs`, `Program.cs`)
|
||||
2. Auth handlers and delegating handlers (`*Handler.cs`, `*DelegatingHandler.cs`)
|
||||
3. Service implementations handling external input or secrets
|
||||
4. Repository and data access code
|
||||
5. Configuration and DI registration (`*Extensions.cs`, `*Options.cs`)
|
||||
6. Validators
|
||||
|
||||
## Review Process
|
||||
|
||||
Execute each phase sequentially. Use the Read tool for files and the Grep tool for pattern searches. NEVER use bash `grep` or `rg` -- always use the Grep tool.
|
||||
|
||||
### Phase 1: Automated Pattern Scanning
|
||||
|
||||
Read `references/scanning-patterns.md` for the full pattern catalog. Run all Grep searches in parallel across `.cs` files in the target scope. Each pattern targets a specific vulnerability class: injection, deserialization, cryptography, async anti-patterns, data exposure, SSRF, missing controls, ReDoS, log injection, open redirect, cookie security, file upload, claims safety, and thread safety.
|
||||
|
||||
### Phase 2: File-by-File Deep Review
|
||||
|
||||
Read `references/deep-review-categories.md` for the complete checklist (Categories A through L). For each file in scope (or top ~20 most security-relevant files when reviewing "all"), check all applicable categories:
|
||||
|
||||
- **A**: Authentication & Authorization (JWT validation, auth schemes, IDOR)
|
||||
- **B**: Input Validation
|
||||
- **C**: Error Handling & Information Leakage
|
||||
- **D**: Cryptography & Secrets
|
||||
- **E**: Data Protection & PII
|
||||
- **F**: Concurrency & State Safety
|
||||
- **G**: CancellationToken Propagation
|
||||
- **H**: HTTP Client Security (resilience handlers, DNS refresh)
|
||||
- **I**: Configuration Security
|
||||
- **J**: Logging & Monitoring Security
|
||||
- **K**: Output Encoding & Response Security
|
||||
- **L**: Supply Chain & Build Security
|
||||
|
||||
### Phase 3: Architecture & Project-Specific Checks
|
||||
|
||||
Read `references/architecture-checks.md` for checks tailored to common ASP.NET Core project patterns. These cover endpoint authorization verification, anonymous endpoint abuse potential, OTP/MFA security, exception handling coverage, optimistic concurrency, state expiry, blob storage SAS security, message queue security, JSON serialization settings, background task queue safety, rate limiting, security headers, middleware ordering, and NuGet audit configuration.
|
||||
|
||||
Read the project's CLAUDE.md or AGENTS.md for project-specific architecture details to inform these checks.
|
||||
|
||||
### Phase 4: Dependency Vulnerability Check
|
||||
|
||||
Read `references/dependencies-and-headers.md` (Phase 4 section) for dependency scanning patterns. Check `.csproj` files for known-vulnerable versions and NuGet audit configuration.
|
||||
|
||||
### Phase 5: Security Headers & Middleware Pipeline
|
||||
|
||||
Read `references/dependencies-and-headers.md` (Phase 5 section) for the 14-item headers checklist and middleware ordering verification.
|
||||
|
||||
## Output Format
|
||||
|
||||
### Security Review Report
|
||||
|
||||
**Scope:** [files/directories reviewed]
|
||||
**Date:** [current date]
|
||||
**Risk Summary:** [X CRITICAL, Y HIGH, Z MEDIUM, W LOW, V INFO]
|
||||
|
||||
#### Findings
|
||||
|
||||
For each finding:
|
||||
|
||||
**[SEVERITY] [SHORT-TITLE]**
|
||||
- **Location:** `file/path.cs:LINE`
|
||||
- **Category:** [OWASP category or security domain]
|
||||
- **Description:** [What the vulnerability is and why it matters]
|
||||
- **Impact:** [What an attacker could achieve]
|
||||
- **Recommendation:** [Specific fix with code example]
|
||||
|
||||
#### Summary Table
|
||||
|
||||
| # | Severity | Category | File | Description |
|
||||
|---|----------|----------|------|-------------|
|
||||
| 1 | CRITICAL | ... | ... | ... |
|
||||
|
||||
#### Recommendations
|
||||
|
||||
1. Immediate fixes (CRITICAL/HIGH)
|
||||
2. Short-term improvements (MEDIUM)
|
||||
3. Long-term hardening (LOW/INFO)
|
||||
4. Tooling recommendations (NuGet audit, SAST integration, etc.)
|
||||
|
||||
## Severity
|
||||
|
||||
Use standard severity: CRITICAL > HIGH > MEDIUM > LOW > INFO. CRITICAL = actively exploitable, HIGH = significant with effort, MEDIUM = increased attack surface, LOW = minor improvement, INFO = hardening suggestion.
|
||||
|
||||
## Anti-Rationalization Table
|
||||
|
||||
| Rationalization | Reality |
|
||||
|---|---|
|
||||
| "This is just a test file" | Test code handling secrets or auth IS production-relevant. Report as INFO. |
|
||||
| "Probably a false positive" | ALWAYS read surrounding code before dismissing. If you cannot prove it safe, report it. |
|
||||
| "The framework handles this" | Verify the protection is actually enabled and configured. Defaults can be overridden. |
|
||||
| "Internal API, not public-facing" | Internal APIs are attacked via SSRF, supply chain, lateral movement. |
|
||||
| "No one would exploit this" | Threat models change. Report it; let the team decide risk acceptance. |
|
||||
|
||||
## Red Flags
|
||||
|
||||
STOP and investigate deeper if you encounter any of these:
|
||||
- Any endpoint without an explicit auth attribute (`[Authorize]` or `[AllowAnonymous]`)
|
||||
- Any `catch` block returning raw exception data to the client
|
||||
- Any hardcoded key, token, password, or connection string literal
|
||||
- Any `new HttpClient()` (should use `IHttpClientFactory`)
|
||||
- Any `TypeNameHandling` value other than `None`
|
||||
|
||||
## Important Guidelines
|
||||
|
||||
1. Only report real findings with evidence (file path and line number). Do not speculate.
|
||||
2. If a pattern search returns no results, note "No issues found" and move on.
|
||||
3. For false positives (e.g., `System.Random` in tests, not production), note as INFO with explanation.
|
||||
4. Prioritize production code over test code.
|
||||
5. When reviewing "all", cap the report at the 30 most significant findings.
|
||||
6. ALWAYS verify context before reporting -- a pattern match alone is not a finding. Read the surrounding code.
|
||||
134
.skills/dotnet-security-review/references/architecture-checks.md
Normal file
134
.skills/dotnet-security-review/references/architecture-checks.md
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
# Architecture & Project-Specific Checks Reference
|
||||
# Phase 3 checks for common ASP.NET Core project patterns. Read the project's CLAUDE.md
|
||||
# or AGENTS.md for project-specific details (endpoint list, service names, DI registrations)
|
||||
# to inform these checks.
|
||||
|
||||
## Check 1: Endpoint Auth Matrix Verification
|
||||
|
||||
Cross-reference the controller's actual `[Authorize]`/`[AllowAnonymous]` attributes against the project's documented auth requirements. Read CLAUDE.md or AGENTS.md for the expected auth matrix. Any mismatch is CRITICAL.
|
||||
|
||||
For projects with multiple auth schemes (e.g., Azure AD + custom JWT), verify each endpoint uses the correct scheme/policy.
|
||||
|
||||
## Check 2: Anonymous Endpoint Abuse Potential
|
||||
|
||||
For each `[AllowAnonymous]` endpoint, verify:
|
||||
- Rate limiting or throttling exists for sensitive operations (e.g., code generation, login attempts)
|
||||
- Enumeration attacks are mitigated (IDs are GUIDs or non-sequential, not auto-increment)
|
||||
- No state modification without prior authentication or verification (e.g., OTP first)
|
||||
|
||||
## Check 3: OTP / MFA Security Review
|
||||
|
||||
If the project implements OTP or MFA, read the service implementation and verify:
|
||||
- Code length is sufficient (6+ characters)
|
||||
- Codes are generated with `RandomNumberGenerator`
|
||||
- Hash is SHA-256 or stronger (not MD5/SHA1)
|
||||
- Expiry is enforced (typically 5-10 minutes)
|
||||
- Wrong attempt counter increments correctly and triggers lockout after a threshold
|
||||
- No timing side-channel in hash comparison
|
||||
|
||||
## Check 4: Exception Handling Coverage
|
||||
|
||||
Grep for `throw new` statements. Verify that all thrown exceptions are either:
|
||||
- The project's structured error type (e.g., `ApiException`, `DomainException`, or the project's custom base exception), OR
|
||||
- Known typed exceptions for external service failures
|
||||
|
||||
Any unstructured exception thrown from handler/service code may bypass error filters and leak internal details.
|
||||
|
||||
## Check 5: Optimistic Concurrency on State Writes
|
||||
|
||||
If using a database with optimistic concurrency (ETags, row versions):
|
||||
- Verify every write/update operation passes the concurrency token
|
||||
- Verify the concurrency token store/tracking mechanism is consulted on every read/write cycle
|
||||
|
||||
## Check 6: Expired State Handling
|
||||
|
||||
If the project uses application-level state expiry (not DB TTL):
|
||||
- Verify expired records are deleted or excluded on read (not returned to callers)
|
||||
- Verify callers cannot act on expired data
|
||||
|
||||
## Check 7: Blob Storage SAS URL Security
|
||||
|
||||
If the project generates SAS URLs for blob storage:
|
||||
- SAS token expiry is short-lived (minutes, not days)
|
||||
- Permission is read-only (not write/delete)
|
||||
- Scoped to the specific blob (not container-level)
|
||||
|
||||
## Check 8: Message Queue Security
|
||||
|
||||
If the project uses message queues (Service Bus, RabbitMQ, etc.):
|
||||
- Messages do not contain secrets or unnecessary PII
|
||||
- Queue connections use managed identity or connection strings from secret stores
|
||||
|
||||
## Check 9: JSON Serialization Settings
|
||||
|
||||
Check that `TypeNameHandling` is set to `None` (default) and not `Auto`/`All` anywhere. This applies to both Newtonsoft.Json and any custom serializer configuration.
|
||||
|
||||
## Check 10: Background Task Queue Safety
|
||||
|
||||
If the project uses a background task queue:
|
||||
- Bounded capacity prevents unbounded memory growth
|
||||
- Backpressure is handled correctly (not silently dropping critical events like audit logs)
|
||||
- Task failures are observed and logged/metered
|
||||
|
||||
## Check 11: Custom Token / JWT Security
|
||||
|
||||
If the project issues its own JWTs (not just validating external tokens):
|
||||
- **Algorithm**: HMAC-SHA256 or stronger (RSA for distributed validation)
|
||||
- **Signing key source**: Key loaded from configuration/secret store, NOT hardcoded
|
||||
- **Signing key length**: Minimum 256 bits (32 bytes) for HMAC-SHA256
|
||||
- **Token expiry**: Appropriately capped (tokens should not outlive the session/resource they protect)
|
||||
- **Claims validation**: Custom claims (e.g., resource IDs) are validated against route parameters by an authorization handler
|
||||
- **TokenValidationParameters**: `ValidateIssuer`, `ValidateAudience`, `ValidateLifetime`, `ValidateIssuerSigningKey` all `true`
|
||||
- **ClockSkew**: Tightened from default 5 minutes to 2 minutes or less
|
||||
|
||||
## Check 12: Response Data Sanitization
|
||||
|
||||
If the project sanitizes response data (e.g., stripping internal paths or fields):
|
||||
- Sanitization handles malformed input gracefully (does not throw/crash)
|
||||
- Only known sensitive fields are stripped (no over-stripping that breaks functionality)
|
||||
- Sanitization is applied on every code path returning the data (not just the happy path)
|
||||
|
||||
## Check 13: Rate Limiting
|
||||
|
||||
Verify rate limiting posture:
|
||||
- Grep for `AddRateLimiter` and `UseRateLimiter` -- if absent, note as finding
|
||||
- Application-level throttling exists for sensitive operations (e.g., SMS/code generation resend limits)
|
||||
- Brute force protection exists for verification endpoints (wrong attempt lockout)
|
||||
- **Recommendation**: Add ASP.NET Core `System.Threading.RateLimiting` middleware for IP-based throttling on public endpoints
|
||||
|
||||
## Check 14: Security Headers Completeness
|
||||
|
||||
Check Program.cs / middleware for these headers (report missing ones):
|
||||
- `X-Content-Type-Options: nosniff`
|
||||
- `X-Frame-Options: DENY`
|
||||
- `Referrer-Policy: strict-origin-when-cross-origin`
|
||||
- `Permissions-Policy: camera=(), microphone=(), geolocation=()`
|
||||
- `X-XSS-Protection: 0` (disable legacy XSS filter; CSP is the modern replacement)
|
||||
- `Content-Security-Policy` (at minimum for APIs: `default-src 'none'`)
|
||||
- `Server` header removed
|
||||
- `X-Powered-By` header removed
|
||||
|
||||
## Check 15: Middleware Pipeline Ordering
|
||||
|
||||
Read Program.cs and verify correct middleware order:
|
||||
1. `UseExceptionHandler` (outermost -- catches everything)
|
||||
2. `UseHsts` (non-development only)
|
||||
3. `UseHttpsRedirection`
|
||||
4. Security headers middleware
|
||||
5. `UseRateLimiter` (if present)
|
||||
6. `UseRouting` (if explicit)
|
||||
7. `UseCors`
|
||||
8. `UseAuthentication`
|
||||
9. `UseAuthorization`
|
||||
10. `MapControllers` / endpoints
|
||||
|
||||
Authentication MUST come before Authorization. CORS MUST come before Authentication. ExceptionHandler MUST be first.
|
||||
|
||||
## Check 16: NuGet Audit & Build Security
|
||||
|
||||
Check for build-level security configuration:
|
||||
- Does `Directory.Build.props` exist? If so, verify `NuGetAudit`, `NuGetAuditMode`, `NuGetAuditLevel` settings.
|
||||
- Are Roslyn security analyzer packages referenced? (`SecurityCodeScan.VS2019`, `SonarAnalyzer.CSharp`, `Meziantou.Analyzer`)
|
||||
- Are `AnalysisLevel` / `AnalysisMode` set in `.csproj` or `Directory.Build.props`?
|
||||
- Run Grep for floating versions: `Version="[^"]*\*"` in `.csproj` files
|
||||
- Recommend `dotnet list package --vulnerable --include-transitive` as a CI step
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
# Deep Review Categories Reference
|
||||
# File-by-file review checklist for Phase 2. For each file in scope (or top ~20 most
|
||||
# security-relevant files when reviewing "all"), read the file and check each applicable category.
|
||||
|
||||
## Category A: Authentication & Authorization
|
||||
|
||||
1. Every controller action has either `[Authorize]` (class or method level) or `[AllowAnonymous]` explicitly.
|
||||
2. No IDOR: when accessing resources by ID, verify the handler checks that the caller owns or is authorized to access that resource.
|
||||
3. JWT validation settings are strict: issuer, audience, lifetime, algorithm all validated.
|
||||
4. Token acquisition uses correct flow: app tokens for backend-to-backend, OBO only where user context is needed.
|
||||
5. No `[AllowAnonymous]` on endpoints that modify sensitive state without alternative authentication (e.g., OTP verification first).
|
||||
6. **Multiple auth scheme verification**: If the project uses multiple auth schemes (e.g., Azure AD + custom JWT), verify correct scheme is applied per endpoint. No scheme confusion between internal and client-facing endpoints.
|
||||
7. **JWT `alg:none` rejection**: Verify `TokenValidationParameters` does NOT allow `alg:none`. All schemes must validate the signing algorithm (`ValidateIssuerSigningKey = true`).
|
||||
8. **HMAC signing key minimum length**: If using HMAC-SHA256 for JWT signing, the key must be at least 256 bits (32 bytes). Check options validation.
|
||||
9. **Structured error responses on auth failure**: `OnChallenge` (401) and `OnForbidden` (403) events should return structured JSON error responses, not default HTML/empty responses.
|
||||
|
||||
## Category B: Input Validation
|
||||
|
||||
1. All DTOs accepted by handlers have corresponding FluentValidation validators registered.
|
||||
2. Route parameters are validated for format before use (e.g., GUID format, positive integers).
|
||||
3. File uploads are validated for content type, size, and extension (not just extension).
|
||||
4. No unvalidated user input flows into file paths, URLs, SQL, commands, or log messages.
|
||||
5. Phone numbers, emails, and other PII are validated and normalized before processing.
|
||||
|
||||
## Category C: Error Handling & Information Leakage
|
||||
|
||||
1. All expected errors use a structured error type -- never return raw exception details to clients.
|
||||
2. Exception filters catch known exception types and return only safe error payloads.
|
||||
3. Unknown exceptions are wrapped as generic 500 errors without stack traces or internal details.
|
||||
4. Error messages returned to clients do not reveal internal architecture, database schema, or file paths.
|
||||
5. Catch blocks never silently swallow exceptions -- they must log or rethrow.
|
||||
|
||||
## Category D: Cryptography & Secrets
|
||||
|
||||
1. OTP/MFA codes use `RandomNumberGenerator` (not `System.Random`).
|
||||
2. Hash comparison uses constant-time comparison to prevent timing attacks.
|
||||
3. Hash storage uses a secure algorithm (SHA-256 minimum; bcrypt/Argon2 for passwords).
|
||||
4. No secrets, connection strings, or API keys appear in source code or `appsettings.json` committed to git.
|
||||
5. Options validation (`ValidateOnStart()`) is configured to reject placeholder secrets in production.
|
||||
|
||||
## Category E: Data Protection & PII
|
||||
|
||||
1. Sensitive fields (phone numbers, etc.) are masked before returning to unauthenticated callers.
|
||||
2. PII (names, addresses, phone numbers, emails) is not logged in full -- use masking.
|
||||
3. Sensitive internal fields (hash values, internal IDs) are excluded from API responses.
|
||||
4. Blob/file storage SAS URLs have appropriate expiry times and permissions (read-only, short-lived).
|
||||
5. Audit logs do not contain raw PII that violates data protection requirements.
|
||||
|
||||
## Category F: Concurrency & State Safety
|
||||
|
||||
1. Database state mutations use optimistic concurrency (ETags, row versions, or equivalent).
|
||||
2. Concurrency exceptions are caught and retried appropriately in handlers.
|
||||
3. Multi-step validation flows (OTP, MFA) handle concurrent attempts correctly.
|
||||
4. Counter increments (e.g., wrong attempt counts) are atomic or protected against race conditions.
|
||||
5. Scheduled/delayed operations do not race with in-progress workflows.
|
||||
|
||||
## Category G: CancellationToken Propagation
|
||||
|
||||
1. Every `async` method in the call chain accepts `CancellationToken cancellationToken = default`.
|
||||
2. The token is passed to every awaited call: HTTP calls, DB queries, blob operations, queue sends.
|
||||
3. The controller passes `HttpContext.RequestAborted` to handlers.
|
||||
4. Missing propagation is a DoS vector (abandoned requests hold resources).
|
||||
|
||||
## Category H: HTTP Client Security
|
||||
|
||||
1. HttpClient instances have timeouts configured (not infinite).
|
||||
2. Delegating handlers do not log tokens or authorization headers.
|
||||
3. SSL/TLS validation is not disabled (`ServerCertificateCustomValidationCallback` returning true).
|
||||
4. Retry policies do not retry on authentication failures (401/403).
|
||||
5. External API clients have adequate timeout and error handling even without resilience middleware.
|
||||
6. **Standard resilience handler**: Verify `AddStandardResilienceHandler()` or equivalent resilience pipeline is configured on named HttpClients.
|
||||
7. **Retry-After header respect**: Retry policies should honor `Retry-After` headers from downstream APIs to avoid cascading failures.
|
||||
8. **DNS refresh**: Verify `SocketsHttpHandler.PooledConnectionLifetime` is set (recommended 2-5 min) to handle DNS changes.
|
||||
|
||||
## Category I: Configuration Security
|
||||
|
||||
1. CORS policy does not use `AllowAnyOrigin()` in production.
|
||||
2. Swagger UI is disabled in production (or restricted to authorized users).
|
||||
3. Health check endpoints do not expose sensitive information.
|
||||
4. `X-Powered-By` and `Server` headers are removed.
|
||||
5. HTTPS redirection and HSTS are configured for production.
|
||||
|
||||
## Category J: Logging & Monitoring Security
|
||||
|
||||
1. Authentication events are logged (both success and failure) for audit trail.
|
||||
2. Authorization failures are logged with sufficient context (user, endpoint, reason).
|
||||
3. Input validation failures are logged (not just returned as 400 responses).
|
||||
4. Structured logging used throughout -- no string interpolation in log method calls (use message templates).
|
||||
5. Sensitive data (passwords, tokens, PII, hash values) is NEVER logged at any log level.
|
||||
6. Correlation IDs are included in all error log entries for traceability.
|
||||
7. Log output is not accessible to API clients (no endpoint returns log data).
|
||||
|
||||
## Category K: Output Encoding & Response Security
|
||||
|
||||
1. No internal file paths, class names, or assembly names leak in API responses (check error messages, headers).
|
||||
2. Razor templates are verified for `@Html.Raw()` usage -- must be justified and input-sanitized.
|
||||
3. `TypeNameHandling.None` verified for Newtonsoft.Json serialization (prevents type injection).
|
||||
4. `Content-Type` headers are explicitly set on all responses (no browser MIME-sniffing).
|
||||
5. Response sanitization logic handles malformed input gracefully (no crashes on invalid JSON/data).
|
||||
|
||||
## Category L: Supply Chain & Build Security
|
||||
|
||||
1. `Directory.Build.props` exists with NuGet audit settings (`NuGetAudit`, `NuGetAuditMode`, `NuGetAuditLevel`).
|
||||
2. Package versions are pinned (no floating versions like `Version="1.*"`).
|
||||
3. `AnalysisLevel` and `AnalysisMode` are set to `latest-Recommended` / `Recommended` in build configuration.
|
||||
4. Security analyzers included in packages (SecurityCodeScan, SonarAnalyzer.CSharp, or Meziantou.Analyzer).
|
||||
5. No known-vulnerable version ranges in `.csproj` files (check Newtonsoft.Json >= 13.0.1, Microsoft.Identity.Web >= 2.x, System.Text.Json >= 8.0.5).
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
# Dependencies & Headers Reference
|
||||
# Combined Phase 4 (dependency vulnerability checks) and Phase 5 (headers/middleware) content.
|
||||
|
||||
## Phase 4: Dependency Vulnerability Check
|
||||
|
||||
### Automated Grep Checks
|
||||
|
||||
Run these Grep patterns against `.csproj` files to detect known-vulnerable version ranges:
|
||||
|
||||
| Pattern | Risk |
|
||||
|---------|------|
|
||||
| `Newtonsoft\.Json.*Version="([0-9]+)` where major < 13 | CVEs in Newtonsoft.Json < 13.0.1 |
|
||||
| `Newtonsoft\.Json.*Version="13\.0\.0"` | Pre-patch 13.x |
|
||||
| `Microsoft\.Identity\.Web.*Version="1\."` | CVEs in Microsoft.Identity.Web < 2.x |
|
||||
| `System\.Text\.Json.*Version="[0-7]\.\|Version="8\.0\.[0-4]"` | CVEs in System.Text.Json < 8.0.5 |
|
||||
| `Version="[^"]*\*"` | Floating versions (unpinned, supply chain risk) |
|
||||
|
||||
### NuGet Audit Configuration Check
|
||||
|
||||
Grep `Directory.Build.props` and `.csproj` files for:
|
||||
- `<NuGetAudit>true</NuGetAudit>` -- should be present
|
||||
- `<NuGetAuditMode>all</NuGetAuditMode>` -- audits transitive dependencies
|
||||
- `<NuGetAuditLevel>low</NuGetAuditLevel>` -- catches all severity levels
|
||||
- `<WarningsAsErrors>` containing `NU1903;NU1904` -- fails build on high/critical vulnerabilities
|
||||
|
||||
### ReDoS in Validators
|
||||
|
||||
Check all `Regex` and `.Matches()` calls in validators:
|
||||
- Pattern: `new Regex\((?!.*RegexOptions\.NonBacktracking)` -- missing NonBacktracking flag (.NET 7+)
|
||||
- Check for nested quantifiers: `(a+)+`, `(a*)*`, `(a|a)*` patterns
|
||||
|
||||
### Command Recommendation
|
||||
|
||||
Include in report output (do NOT run automatically):
|
||||
```bash
|
||||
dotnet list package --vulnerable --include-transitive
|
||||
```
|
||||
|
||||
## Phase 5: Security Headers & Middleware Pipeline
|
||||
|
||||
### Headers Checklist (14 items)
|
||||
|
||||
Read `Program.cs` and any middleware configuration files. Check for each header:
|
||||
|
||||
| # | Header / Control | Expected Value | Severity if Missing |
|
||||
|---|-----------------|----------------|---------------------|
|
||||
| 1 | `X-Content-Type-Options` | `nosniff` | MEDIUM |
|
||||
| 2 | `X-Frame-Options` | `DENY` | MEDIUM |
|
||||
| 3 | `Referrer-Policy` | `strict-origin-when-cross-origin` | LOW |
|
||||
| 4 | `Permissions-Policy` | `camera=(), microphone=(), geolocation=()` | LOW |
|
||||
| 5 | `X-XSS-Protection` | `0` (disable legacy filter; CSP replaces it) | LOW |
|
||||
| 6 | `Content-Security-Policy` | At minimum `default-src 'none'` for APIs | MEDIUM |
|
||||
| 7 | `Server` header | REMOVED | LOW |
|
||||
| 8 | `X-Powered-By` header | REMOVED | LOW |
|
||||
| 9 | `Strict-Transport-Security` | `max-age=31536000; includeSubDomains; preload` | HIGH |
|
||||
| 10 | `Cache-Control` | `no-store` on sensitive data endpoints | MEDIUM |
|
||||
| 11 | HTTPS Redirection | `app.UseHttpsRedirection()` present | HIGH |
|
||||
| 12 | HSTS | `app.UseHsts()` in non-development | HIGH |
|
||||
| 13 | Rate Limiting | `app.UseRateLimiter()` present | MEDIUM |
|
||||
| 14 | Swagger restricted | Conditionally enabled (dev/staging only) | MEDIUM |
|
||||
|
||||
### Middleware Pipeline Ordering
|
||||
|
||||
The correct order for ASP.NET Core middleware is critical. Misordering can bypass security controls.
|
||||
|
||||
**Expected order:**
|
||||
```
|
||||
1. app.UseExceptionHandler(...) // Outermost: catches all unhandled exceptions
|
||||
2. app.UseHsts() // HSTS (non-development only)
|
||||
3. app.UseHttpsRedirection() // Force HTTPS
|
||||
4. Security headers middleware // Custom: X-Content-Type-Options, etc.
|
||||
5. app.UseRateLimiter() // Throttle before routing (if present)
|
||||
6. app.UseRouting() // (implicit in .NET 8+ with MapControllers)
|
||||
7. app.UseCors(...) // CORS before auth (preflight must not require auth)
|
||||
8. app.UseAuthentication() // Identify the caller
|
||||
9. app.UseAuthorization() // Enforce access rules
|
||||
10. app.MapControllers() // Endpoint dispatch
|
||||
```
|
||||
|
||||
**Critical ordering rules:**
|
||||
- `UseExceptionHandler` MUST be first -- otherwise exceptions in early middleware are unhandled
|
||||
- `UseAuthentication` MUST come before `UseAuthorization` -- otherwise auth policies have no identity to check
|
||||
- `UseCors` MUST come before `UseAuthentication` -- otherwise CORS preflight (OPTIONS) requests fail with 401
|
||||
- `UseRateLimiter` SHOULD come before `UseRouting` -- otherwise rate limits apply after route matching overhead
|
||||
- `UseHsts` and `UseHttpsRedirection` SHOULD come early -- before any response body is written
|
||||
|
||||
### Middleware Verification Procedure
|
||||
|
||||
1. Read `Program.cs` from the `var app = builder.Build()` line to `app.Run()`
|
||||
2. List every `app.Use*` and `app.Map*` call in order
|
||||
3. Compare against expected order above
|
||||
4. Report any misordering as MEDIUM severity
|
||||
116
.skills/dotnet-security-review/references/scanning-patterns.md
Normal file
116
.skills/dotnet-security-review/references/scanning-patterns.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
# Scanning Patterns Reference
|
||||
# Automated Grep patterns organized by vulnerability class for Phase 1 scanning.
|
||||
# Run all searches in parallel across .cs files in the target scope.
|
||||
|
||||
## Injection Vulnerabilities
|
||||
|
||||
| ID | Pattern | Target |
|
||||
|----|---------|--------|
|
||||
| INJ-1 | `\$".*SELECT\|INSERT\|UPDATE\|DELETE\|DROP\|EXEC` | SQL injection via string interpolation |
|
||||
| INJ-2 | `string\.Format.*SELECT\|INSERT\|UPDATE\|DELETE` | SQL injection via string.Format |
|
||||
| INJ-3 | `\.FromSqlRaw\(.*\$"\|\.FromSqlRaw\(.*string\.Format` | EF Core raw SQL injection |
|
||||
| INJ-4 | `ExecuteSqlRaw\(.*\$"\|ExecuteSqlRaw\(.*string\.Format` | EF Core command injection |
|
||||
| INJ-5 | `Process\.Start\|ProcessStartInfo` | Command injection |
|
||||
| INJ-6 | `DirectorySearcher\|LdapConnection` | LDAP injection (check for string concat) |
|
||||
| INJ-7 | `XmlDocument\|XmlReader\|XDocument` | XXE (verify secure settings) |
|
||||
| INJ-8 | `Path\.Combine.*Request\|Path\.Combine.*user\|\.\.\/\|\.\.\\` | Path traversal |
|
||||
|
||||
## Insecure Deserialization
|
||||
|
||||
| ID | Pattern | Target |
|
||||
|----|---------|--------|
|
||||
| DES-1 | `BinaryFormatter\|SoapFormatter\|ObjectStateFormatter\|LosFormatter\|NetDataContractSerializer` | Banned deserializers |
|
||||
| DES-2 | `JsonConvert\.DeserializeObject.*TypeNameHandling` | Newtonsoft type handling |
|
||||
| DES-3 | `TypeNameHandling\s*=\s*TypeNameHandling\.(All\|Auto\|Objects\|Arrays)` | Unsafe type handling |
|
||||
|
||||
## Cryptography Weaknesses
|
||||
|
||||
| ID | Pattern | Target |
|
||||
|----|---------|--------|
|
||||
| CRY-1 | `new Random\(\)\|System\.Random` | Insecure randomness (should be RandomNumberGenerator) |
|
||||
| CRY-2 | `MD5\.Create\|SHA1\.Create\|DESCryptoServiceProvider\|RC2CryptoServiceProvider\|TripleDES` | Weak algorithms |
|
||||
| CRY-3 | `ECB` | Insecure cipher mode |
|
||||
| CRY-4 | `password\|secret\|key\|token\|credential\|apikey\|connectionstring` in string literals | Hardcoded secrets |
|
||||
|
||||
## Async Anti-Patterns
|
||||
|
||||
| ID | Pattern | Target |
|
||||
|----|---------|--------|
|
||||
| ASY-1 | `\.Result[^s]\|\.Result$` | Sync-over-async deadlock risk |
|
||||
| ASY-2 | `\.Wait\(\)` | Sync-over-async deadlock risk |
|
||||
| ASY-3 | `\.GetAwaiter\(\)\.GetResult\(\)` | Sync-over-async |
|
||||
| ASY-4 | `Task\.Run\(` | Thread pool abuse in ASP.NET context |
|
||||
|
||||
## Sensitive Data Exposure
|
||||
|
||||
| ID | Pattern | Target |
|
||||
|----|---------|--------|
|
||||
| EXP-1 | `_logger\.Log.*password\|_logger\.Log.*secret\|_logger\.Log.*token\|_logger\.Log.*apiKey` (case insensitive) | Logging secrets |
|
||||
| EXP-2 | `Console\.Write.*password\|Console\.Write.*secret\|Console\.Write.*token` | Console output of secrets |
|
||||
| EXP-3 | `Html\.Raw\(` | XSS via unencoded HTML |
|
||||
| EXP-4 | `Exception\.ToString\(\)\|Exception\.StackTrace\|Exception\.Message` returned in HTTP responses | Stack trace leakage |
|
||||
|
||||
## SSRF Risks
|
||||
|
||||
| ID | Pattern | Target |
|
||||
|----|---------|--------|
|
||||
| SSRF-1 | `new HttpClient\(\).*\+\|HttpClient.*GetAsync\(.*\+\|HttpClient.*PostAsync\(.*\+` | User-controlled URLs |
|
||||
| SSRF-2 | `new Uri\(.*Request\|new Uri\(.*user\|new Uri\(.*input` | Unvalidated URI construction |
|
||||
| SSRF-3 | `HttpClient.*GetAsync\(.*[^"]\)\|HttpClient.*PostAsync\(.*[^"]\)` | Non-literal URLs in HTTP calls |
|
||||
| SSRF-4 | `new Uri\([^"]*\)` | Dynamic URI construction |
|
||||
| SSRF-5 | `IPAddress\.Parse\("\|Uri\("http` | Hardcoded IPs/URLs |
|
||||
|
||||
## Missing Security Controls
|
||||
|
||||
| ID | Pattern | Target |
|
||||
|----|---------|--------|
|
||||
| CTL-1 | `\[HttpPost\]\|\[HttpPut\]\|\[HttpDelete\]\|\[HttpPatch\]` | Unannotated endpoints (check for nearby [Authorize]/[AllowAnonymous]) |
|
||||
| CTL-2 | `AllowAnyOrigin` | CORS misconfiguration |
|
||||
| CTL-3 | `app\.UseDeveloperExceptionPage` | Dev error page in production |
|
||||
| CTL-4 | `#pragma warning disable` | Disabled security warnings |
|
||||
|
||||
## ReDoS
|
||||
|
||||
| ID | Pattern | Target |
|
||||
|----|---------|--------|
|
||||
| REG-1 | `new Regex\((?!.*RegexOptions\.NonBacktracking)` | Regex without NonBacktracking (ReDoS risk in .NET 7+) |
|
||||
|
||||
## Log Injection
|
||||
|
||||
| ID | Pattern | Target |
|
||||
|----|---------|--------|
|
||||
| LOG-1 | `_logger\.Log.*(Request\.Query\|Request\.Form\|Request\.Headers\|Request\.Body)` | Unsanitized request data in logs |
|
||||
| LOG-2 | `_logger\.Log.*\\n\|_logger\.Log.*\\r` | Newline chars in log messages (log forging) |
|
||||
|
||||
## Open Redirect
|
||||
|
||||
| ID | Pattern | Target |
|
||||
|----|---------|--------|
|
||||
| RED-1 | `Redirect\(\|RedirectToAction\(.*\+` | Open redirect via concatenation |
|
||||
| RED-2 | `Response\.Redirect\(` | Direct response redirect |
|
||||
|
||||
## Cookie Security
|
||||
|
||||
| ID | Pattern | Target |
|
||||
|----|---------|--------|
|
||||
| COK-1 | `CookieOptions\|\.Cookies\.Append` | Cookie usage (verify HttpOnly, Secure, SameSite) |
|
||||
| COK-2 | `SameSite\s*=\s*SameSiteMode\.None` | SameSite=None (requires Secure flag) |
|
||||
|
||||
## File Upload
|
||||
|
||||
| ID | Pattern | Target |
|
||||
|----|---------|--------|
|
||||
| UPL-1 | `IFormFile` | File upload handling (verify validation) |
|
||||
| UPL-2 | `ContentType.*application/octet-stream\|ContentType.*\*\/\*` | Permissive content type acceptance |
|
||||
|
||||
## Claims Safety
|
||||
|
||||
| ID | Pattern | Target |
|
||||
|----|---------|--------|
|
||||
| CLM-1 | `User\.Claims\.First\(\|User\.FindFirst\(.*\.Value(?!\?)` | Null-unsafe claims access (missing ?.) |
|
||||
|
||||
## Thread Safety
|
||||
|
||||
| ID | Pattern | Target |
|
||||
|----|---------|--------|
|
||||
| THR-1 | `static\s+.*HttpClient\s+\w+\s*=\s*new\s+HttpClient` | Static HttpClient instantiation (use IHttpClientFactory) |
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
---
|
||||
name: general-prompt-engineer
|
||||
description: create, repair, compress, and optimize prompts, system messages, tool instructions, schemas, and eval rubrics for general tasks across writing, research, coding, analysis, planning, tutoring, automation, and agent workflows. use when the user wants a new prompt, wants an existing prompt improved, wants prompt failures debugged, or needs better structure for grounding, tool use, output format, or reliability.
|
||||
model: claude-opus-4-8
|
||||
effort: xhigh
|
||||
---
|
||||
|
||||
# Prompt Engineer
|
||||
|
|
|
|||
|
|
@ -1,406 +0,0 @@
|
|||
---
|
||||
name: mcc-prompt-engineer
|
||||
description: >
|
||||
Manually triggered skill for the Minecraft Console Client (MCC) project
|
||||
(https://github.com/MCCTeam/Minecraft-Console-Client). Invoke this skill
|
||||
when the user wants to create, design, or generate a high-quality prompt for
|
||||
addressing any MCC-related development request -- bug fixes, new features,
|
||||
refactors, protocol work, authentication, bot scripting, or architecture
|
||||
decisions. The skill interviews the user, explores the MCC codebase via
|
||||
sub-agents, identifies relevant project skills, and synthesises everything
|
||||
into a state-of-the-art, self-contained prompt that includes an embedded
|
||||
reasoning framework, plan-mode directives, skill references, and targeted
|
||||
sub-agent instructions. Do NOT trigger automatically; wait for the user to
|
||||
explicitly invoke it (e.g. "generate a prompt for...", "build me a prompt",
|
||||
"/mcc-prompt-engineer", or "use the MCC prompt skill").
|
||||
compatibility: "Claude Code, Cursor, Codex, GitHub Copilot, Windsurf, and any AI coding agent. Optional tools: AskUserQuestion, Task, WebSearch, plan."
|
||||
---
|
||||
|
||||
# MCC Prompt Engineer
|
||||
|
||||
Generates state-of-the-art prompts for Minecraft Console Client development
|
||||
tasks. Combines live codebase knowledge (via sub-agents and AGENTS.md),
|
||||
structured prompt engineering patterns, an embedded ULTRATHINK reasoning
|
||||
framework, and the MCC project's skill ecosystem so the produced prompt is
|
||||
immediately ready to use in any AI coding agent.
|
||||
|
||||
---
|
||||
|
||||
## Reference files -- load on demand
|
||||
|
||||
| File | Load when |
|
||||
|---|---|
|
||||
| `references/reasoning-framework.md` | Embedding the ULTRATHINK protocol into the generated prompt |
|
||||
| `references/prompt-patterns.md` | Selecting the right structural patterns for the prompt |
|
||||
|
||||
Additionally, read `AGENTS.md` at the repository root early in the process.
|
||||
It contains the authoritative codebase map -- module responsibilities, key
|
||||
file paths, architecture overview, version support table, and engineering
|
||||
DO/DON'T guidance -- and replaces the need for broad exploratory file reads.
|
||||
|
||||
---
|
||||
|
||||
## Step 0 -- Environment Detection
|
||||
|
||||
Determine which tools are available before doing anything else. This gates how
|
||||
you ask questions and spawn sub-agents.
|
||||
|
||||
```
|
||||
Claude Code -> AskUserQuestion and Task tools; plan mode via "plan" tool
|
||||
or /plan command.
|
||||
Cursor / Codex -> No AskUserQuestion; ask clarifying questions inline as a
|
||||
numbered list; sub-agents via parallel tool calls where
|
||||
supported, otherwise inline.
|
||||
GitHub Copilot -> Similar to Cursor; use runSubagent where available.
|
||||
Other agents -> Fall back to inline questions and sequential exploration.
|
||||
```
|
||||
|
||||
Record your environment determination internally before continuing.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 -- Parse the Request
|
||||
|
||||
Extract everything the user has stated. Do not invent requirements or make
|
||||
assumptions yet. Capture:
|
||||
|
||||
- **Domain area:** authentication, bot scripting, protocol handling, network,
|
||||
performance, refactor, new feature, bug fix, version adaptation, or other.
|
||||
- **Stated goal:** what the user wants to achieve.
|
||||
- **Known constraints:** language version (C# 14 / .NET 10), compatibility
|
||||
requirements, scope limits (additive-only, etc.).
|
||||
- **References provided:** URLs, file paths, issue numbers, error messages.
|
||||
- **Ambiguity level:** High (proceed) / Medium (note gaps) / Low (clarify
|
||||
before continuing).
|
||||
|
||||
---
|
||||
|
||||
## Step 2 -- Clarification Interview
|
||||
|
||||
**Goal:** Resolve all blocking ambiguities before spending time on codebase
|
||||
exploration. Unblocking questions first saves sub-agent round-trips.
|
||||
|
||||
### If in Claude Code
|
||||
Use the `AskUserQuestion` tool. Ask all questions in a single call -- do not
|
||||
drip-feed questions turn by turn.
|
||||
|
||||
### In any other environment
|
||||
Print a numbered list of questions. Wait for answers before proceeding.
|
||||
|
||||
### Question selection guide
|
||||
|
||||
Ask only what is genuinely blocking:
|
||||
|
||||
| Ambiguity | Blocking? | Example question |
|
||||
|---|---|---|
|
||||
| Scope of change (additive vs rewrite) | Yes | "Should this be additive, or can it replace existing code?" |
|
||||
| Target .NET / C# version | Yes if non-obvious | "Which .NET version -- 8, 10, or latest?" |
|
||||
| Auth flow variant | Yes for auth tasks | "Device-code flow, interactive browser, or both?" |
|
||||
| Performance constraints | Usually no | Skip unless the user mentioned perf |
|
||||
| Test coverage expectation | Sometimes | "Do you want unit tests, or integration guidance only?" |
|
||||
|
||||
**Always ask:**
|
||||
1. "Is there a specific file, class, or method you already know is the right
|
||||
starting point?"
|
||||
2. "Are there any hard constraints -- things the solution must NOT do or touch?"
|
||||
|
||||
Offer a best-guess assumption alongside each question so the user can confirm
|
||||
or correct rather than answer from scratch.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 -- Codebase Exploration
|
||||
|
||||
Start by reading `AGENTS.md` at the repository root. It provides the
|
||||
authoritative module map, architecture overview, version support table, and
|
||||
engineering DO/DON'T guidance. Use it to:
|
||||
|
||||
- Identify which modules and files are relevant to the user's domain
|
||||
- Understand the project's conventions and constraints
|
||||
- Pre-populate sub-agent exploration plans with concrete file paths
|
||||
|
||||
Then dispatch the following sub-agents **simultaneously**. Each must return a
|
||||
concise written summary only -- raw file contents and grep output waste context
|
||||
and degrade reasoning quality downstream (context rot).
|
||||
|
||||
### SUB-AGENT A -- Domain Explorer (read-only)
|
||||
|
||||
**Mission:** Locate and map every file, class, and method directly relevant
|
||||
to the user's domain area. Scope your search using the module map from
|
||||
AGENTS.md rather than exploring the entire repository.
|
||||
|
||||
**Scoped exploration plan (fill in before dispatching):**
|
||||
```
|
||||
Files / directories to read:
|
||||
[derived from AGENTS.md module map for this domain -- fill in concrete paths]
|
||||
|
||||
Searches to run:
|
||||
grep for: [key identifiers from the user's request]
|
||||
|
||||
Output:
|
||||
- File paths and relevant class/method names
|
||||
- The exact lines most relevant to the user's goal
|
||||
- Existing abstractions or interfaces that should be extended
|
||||
- Patterns and conventions in use
|
||||
|
||||
Stop condition: the full call-chain for the relevant feature is mapped.
|
||||
```
|
||||
|
||||
### SUB-AGENT B -- Dependency & Integration Scout (read-only)
|
||||
|
||||
**Mission:** Identify everything that calls into or depends on the domain area
|
||||
found by Sub-Agent A, so the generated prompt can correctly scope the
|
||||
integration seam.
|
||||
|
||||
**Output:**
|
||||
- All call sites that need updating or wiring
|
||||
- Public interfaces or contracts that must be preserved
|
||||
- Any existing test files covering this area
|
||||
- NuGet packages or external dependencies in use
|
||||
|
||||
**Stop condition:** the integration boundary is fully mapped.
|
||||
|
||||
### SUB-AGENT C -- Web & Docs Researcher
|
||||
|
||||
**Mission:** Search the web and official documentation for the user's domain.
|
||||
Always search the web -- do not limit research to the codebase.
|
||||
|
||||
**Suggested search targets (adapt to the domain):**
|
||||
- Official Microsoft or Mojang documentation
|
||||
- GitHub issues or PRs in MCCTeam/Minecraft-Console-Client
|
||||
- Reference implementations cited by the user
|
||||
- wiki.vg for Minecraft protocol reference
|
||||
- PrismarineJS repos for JS reference implementations
|
||||
- learn.microsoft.com for .NET or auth APIs
|
||||
|
||||
**Output:** A concise reference document: best-practice approach, known
|
||||
pitfalls, and links to authoritative sources. Flag conflicting information.
|
||||
|
||||
Await all sub-agent summaries before proceeding to Step 4.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 -- Skill Discovery
|
||||
|
||||
Scan the `.claude/skills/` directory in the project root. Read the YAML
|
||||
frontmatter (name + description) from each skill's `SKILL.md`. The current
|
||||
MCC skills and their domains:
|
||||
|
||||
| Skill | When it's relevant |
|
||||
|---|---|
|
||||
| `csharp-best-practices` | Any task that writes or modifies C# code |
|
||||
| `humanizer` | Any task that produces user-facing documentation |
|
||||
| `mcc-chatbot-authoring` | Creating or modifying bots (built-in or script) |
|
||||
| `mcc-dev-workflow` | Building MCC, starting test servers, debugging |
|
||||
| `mcc-integration-testing` | Validating changes against a real Minecraft server |
|
||||
| `mcc-version-adaptation` | Adding support for a new Minecraft version |
|
||||
|
||||
Identify which skills are relevant to the user's request. Record them for
|
||||
inclusion in the generated prompt's `<available_skills>` block.
|
||||
|
||||
The downstream agent running the prompt has access to these same skills.
|
||||
Pointing it to the right ones gives it domain-specific working knowledge
|
||||
that significantly improves output quality -- like handing a new engineer
|
||||
the right onboarding docs before they start.
|
||||
|
||||
---
|
||||
|
||||
## Step 5 -- Synthesis
|
||||
|
||||
Combine the sub-agent summaries, user answers, AGENTS.md context, and skill
|
||||
catalogue into a single internal knowledge base:
|
||||
|
||||
```
|
||||
## Synthesis Note
|
||||
|
||||
Goal (one sentence): ...
|
||||
Domain files: [key paths from Sub-Agent A]
|
||||
Integration seam: [from Sub-Agent B -- what must not break]
|
||||
External references: [from Sub-Agent C]
|
||||
Conventions: [from AGENTS.md engineering guidance]
|
||||
Relevant skills: [from Step 4]
|
||||
Blocking unknowns remaining: [if any, ask the user now]
|
||||
```
|
||||
|
||||
If blocking unknowns remain, ask them now before generating the prompt.
|
||||
|
||||
---
|
||||
|
||||
## Step 6 -- Generate the Prompt
|
||||
|
||||
Read `references/reasoning-framework.md` and `references/prompt-patterns.md`
|
||||
now if you have not already.
|
||||
|
||||
Build the final prompt using the **Prompt Assembly Checklist** below. Every
|
||||
item must be addressed -- a missing item is a prompt defect.
|
||||
|
||||
### Prompt Assembly Checklist
|
||||
|
||||
- [ ] `<role>` block: domain expert covering all relevant technologies.
|
||||
- [ ] `<context>` block: synthesised from user goal + sub-agent findings.
|
||||
Include the exact error message or failure mode if provided.
|
||||
Pre-answer known facts so the downstream agent does not re-derive them.
|
||||
- [ ] `<agents_md>` directive: instruct the agent to read AGENTS.md for the
|
||||
module map, architecture, and engineering guidance.
|
||||
- [ ] `<available_skills>` block: list the relevant skills from Step 4 with
|
||||
file paths and when to load each one.
|
||||
- [ ] `<reasoning_protocol>` block: adapted ULTRATHINK framework.
|
||||
Phase 0 orientation pre-answered where certain.
|
||||
Phase 1 requirements pre-seeded from the synthesis note.
|
||||
Phase 2 decomposition pre-seeded with sub-tasks.
|
||||
Phase 2D exploration plan pre-populated with real file paths.
|
||||
Phase 4 self-validation items domain-specific and verifiable.
|
||||
- [ ] Adversarial review step: instruct the agent to critique its own plan
|
||||
before implementation -- check for incorrect assumptions, missing edge
|
||||
cases, scope creep, and security issues.
|
||||
- [ ] Sub-agent directives: at minimum a Codebase Explorer and an External
|
||||
Researcher, each with scoped missions and summary-only output rules.
|
||||
- [ ] Plan mode directive: must appear before Phase 0. Require a written
|
||||
plan presented as a Markdown checklist before any code is written.
|
||||
- [ ] `<design_goals>` block: 3-6 measurable, verifiable goals.
|
||||
- [ ] `<scope_constraint>` block: name specific directories, classes, or
|
||||
files that must NOT be touched.
|
||||
- [ ] `<output_format>` block: ordered delivery -- planning artefacts first,
|
||||
then implementation files.
|
||||
- [ ] Web search mandate in at least one sub-agent directive.
|
||||
- [ ] Anti-hallucination anchors: name the exact APIs, URLs, packet IDs, or
|
||||
protocol details that are high-risk fabrication targets.
|
||||
- [ ] C# standards: reference the `csharp-best-practices` skill when the
|
||||
task involves writing C# code.
|
||||
|
||||
### Prompt structure template
|
||||
|
||||
Use this XML skeleton. Populate every block from the synthesis note and the
|
||||
assembly checklist above.
|
||||
|
||||
```xml
|
||||
<role>
|
||||
[Domain expert covering: C# 14 / .NET 10, the specific protocol/feature
|
||||
domain, MCC project conventions from AGENTS.md]
|
||||
</role>
|
||||
|
||||
<context>
|
||||
[User goal restated. Known error or failure mode. Why the current state
|
||||
is insufficient. What "done" looks like. Key facts pre-answered.]
|
||||
</context>
|
||||
|
||||
<agents_md>
|
||||
Read AGENTS.md at the repository root before starting implementation.
|
||||
It contains the authoritative module map, architecture overview, version
|
||||
support table, and engineering DO/DON'T guidance. Use it to orient yourself
|
||||
and scope your exploration. When AGENTS.md and other docs disagree, prefer
|
||||
current code, then AGENTS.md.
|
||||
</agents_md>
|
||||
|
||||
<available_skills>
|
||||
The following project skills are at .claude/skills/ and should be loaded
|
||||
(by reading their SKILL.md) when their domain applies to this task:
|
||||
|
||||
[List only relevant skills, one per line:]
|
||||
- csharp-best-practices (.claude/skills/csharp-best-practices/SKILL.md):
|
||||
Read before writing or reviewing any C# code.
|
||||
- [other relevant skills...]
|
||||
|
||||
Load skills just-in-time as you reach relevant work, not all upfront.
|
||||
</available_skills>
|
||||
|
||||
<reasoning_protocol>
|
||||
## Plan Before Code (non-negotiable)
|
||||
|
||||
Before writing any implementation code, produce and present a complete
|
||||
written plan as a Markdown checklist. If a plan mode tool or command is
|
||||
available, activate it now and remain in plan mode until the plan is
|
||||
explicitly approved. Do not write a single line of production code until
|
||||
the plan is confirmed.
|
||||
|
||||
[Adapted ULTRATHINK framework from references/reasoning-framework.md.
|
||||
Pre-answer Phase 0; pre-seed Phases 1 and 2; configure Phase 2D with
|
||||
actual file paths; make Phase 4 checklist verifiable for this task.
|
||||
|
||||
Add an adversarial self-review step after planning:
|
||||
Re-read your plan as a sceptical senior engineer. Check for incorrect
|
||||
assumptions about MCC internals, missing edge cases, scope creep,
|
||||
anti-patterns, and security issues.]
|
||||
</reasoning_protocol>
|
||||
|
||||
<design_goals>
|
||||
[3-6 measurable, verifiable goals. Each checkable with a yes/no answer.]
|
||||
</design_goals>
|
||||
|
||||
<scope_constraint>
|
||||
[What must NOT be modified. Name specific directories, classes, or files.
|
||||
What must remain backwards-compatible. What to avoid even if it seems
|
||||
helpful.]
|
||||
</scope_constraint>
|
||||
|
||||
<output_format>
|
||||
[Ordered: planning artefacts first (checklist, design decisions, critique
|
||||
summary), then implementation files, then compliance report.]
|
||||
</output_format>
|
||||
```
|
||||
|
||||
### Sub-agent output discipline
|
||||
|
||||
Every sub-agent directive in the generated prompt must include:
|
||||
|
||||
> "Return a concise written summary only. Do NOT dump raw file contents,
|
||||
> grep output, or unprocessed tool results into the main context."
|
||||
|
||||
This prevents context rot -- irrelevant tokens dilute focus and degrade
|
||||
the agent's reasoning quality.
|
||||
|
||||
---
|
||||
|
||||
## Step 7 -- Prompt Quality Gate
|
||||
|
||||
Before delivering, verify every item:
|
||||
|
||||
```
|
||||
- [ ] Every block (<role>, <context>, <agents_md>, <available_skills>,
|
||||
<reasoning_protocol>, <design_goals>, <scope_constraint>,
|
||||
<output_format>) is present and non-empty.
|
||||
- [ ] The prompt directs the agent to read AGENTS.md for orientation.
|
||||
- [ ] <available_skills> lists the correct skills for this task's domain.
|
||||
- [ ] Phase 2D has actual file paths, not generic placeholders.
|
||||
- [ ] Plan mode directive appears before Phase 0.
|
||||
- [ ] All sub-agents have scoped missions and summary-only output rules.
|
||||
- [ ] At least one sub-agent has an explicit web search mandate.
|
||||
- [ ] Phase 4 items are objectively verifiable for THIS task.
|
||||
- [ ] Anti-hallucination anchors target this domain's fabrication risks.
|
||||
- [ ] Scope constraint is specific enough to prevent accidental drift.
|
||||
- [ ] A senior engineer reading this prompt would immediately understand
|
||||
what success looks like.
|
||||
```
|
||||
|
||||
Fix any unchecked items before delivering.
|
||||
|
||||
---
|
||||
|
||||
## Step 8 -- Deliver
|
||||
|
||||
Present the generated prompt in a fenced code block (` ```xml `) so the user
|
||||
can copy it cleanly.
|
||||
|
||||
Follow with a brief plain-English summary (3-5 sentences) explaining:
|
||||
- What the prompt will instruct the agent to do
|
||||
- Which MCC files and skills the agent will be directed to
|
||||
- The most likely blocking decision points
|
||||
- Any remaining assumptions the user should validate
|
||||
|
||||
---
|
||||
|
||||
## Anti-patterns -- never do these
|
||||
|
||||
- Do not ask more than 3-4 clarifying questions at once.
|
||||
- Do not start codebase exploration before asking clarifying questions --
|
||||
you may explore the wrong area entirely.
|
||||
- Do not generate a prompt that skips the planning phase.
|
||||
- Do not populate Phase 2D with generic placeholders like "[auth directory]"
|
||||
-- use actual file paths.
|
||||
- Do not produce a prompt with vague scope constraints. "Don't touch
|
||||
unrelated code" requires the agent to guess. Name the specific files
|
||||
and directories that are out of bounds.
|
||||
- Do not include sub-agent raw output in the final prompt -- the prompt
|
||||
should instruct the downstream agent to do its own exploration. Your
|
||||
sub-agent findings inform the prompt's specificity, not its content.
|
||||
- Do not list skills in `<available_skills>` that are irrelevant to the task.
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
# Prompt Engineering Patterns for MCC Tasks
|
||||
# Reference file — load when selecting structural patterns for the generated prompt
|
||||
|
||||
---
|
||||
|
||||
## Core Principles (Anthropic / 2025–2026 Best Practices)
|
||||
|
||||
### 1. Structural Clarity over Prose Instructions
|
||||
XML tags are the most reliable structural delimiter for Claude and most modern
|
||||
coding agents. Use `<role>`, `<context>`, `<reasoning_protocol>`,
|
||||
`<design_goals>`, `<scope_constraint>`, and `<output_format>` consistently.
|
||||
Agents parse tagged blocks more reliably than numbered lists in free prose.
|
||||
|
||||
### 2. Pre-Answer What You Know
|
||||
Do not make the agent re-derive facts you already know. If codebase exploration
|
||||
has identified the exact failing file and line, put it in `<context>`. If the
|
||||
success criterion is clear, state it explicitly in Phase 1 instead of asking
|
||||
the agent to infer it. Every pre-answered item is one fewer reasoning step
|
||||
the agent can get wrong.
|
||||
|
||||
### 3. Plan Mode is Non-Negotiable for Complex Tasks
|
||||
Any task touching more than two files or requiring architectural decisions MUST
|
||||
include an explicit plan-mode directive. Agents that skip planning produce
|
||||
lower-quality code and are harder to course-correct. The directive must appear
|
||||
before Phase 0 so it gates the entire session.
|
||||
|
||||
### 4. Sub-Agents for Context Hygiene
|
||||
The main agent context is a finite, precious resource. Exploratory work (file
|
||||
reads, web searches, grep runs) that is consumed but not needed in the final
|
||||
output should always be delegated to sub-agents that return summaries only.
|
||||
Keyword: "Return a concise written summary. Do NOT dump raw output into the
|
||||
main context."
|
||||
|
||||
### 5. Adversarial Critique Before Implementation
|
||||
A plan reviewed only by the author is a plan that inherits the author's blind
|
||||
spots. Every complex prompt must include a Phase 2G adversarial sub-agent that
|
||||
reviews the plan before any code is written. This is the single highest-ROI
|
||||
addition to any agentic prompt.
|
||||
|
||||
### 6. Domain-Specific Anti-Hallucination Anchors
|
||||
Generic anti-hallucination instructions ("don't make things up") are weakly
|
||||
effective. Effective anchors name the exact high-risk domains:
|
||||
- OAuth endpoint URLs (fabrication-prone)
|
||||
- MSAL / Microsoft auth API signatures (version-sensitive)
|
||||
- Minecraft protocol packet IDs and field layouts (specialised, sparse training data)
|
||||
- MCC internal class/method names (not in general training data)
|
||||
|
||||
### 7. Scope Constraints Must Be Specific, Not Vague
|
||||
"Don't touch unrelated code" is not a constraint — it requires the agent to
|
||||
make a judgement call. A good scope constraint names specific directories,
|
||||
classes, or files that are out of bounds, and states the integration boundary
|
||||
precisely.
|
||||
|
||||
### 8. Output Format as a Delivery Contract
|
||||
The `<output_format>` block is a contract, not a suggestion. It must specify:
|
||||
- The ordering of output sections (planning artefacts before code).
|
||||
- File naming conventions.
|
||||
- Code block format (fenced, with filename on the opening fence line).
|
||||
- Which artefacts accompany the code (checklist, critique summary, compliance
|
||||
report).
|
||||
|
||||
---
|
||||
|
||||
## Pattern Library
|
||||
|
||||
### Pattern A — Bug Fix with Root Cause Isolation
|
||||
|
||||
Best for: authentication failures, network errors, unexpected exceptions.
|
||||
|
||||
Key additions to the reasoning protocol:
|
||||
- Phase 1.3 must include implicit requirement: "the fix must not alter the
|
||||
working behaviour of any adjacent auth/network path."
|
||||
- Phase 2D exploration plan must identify both the failing path AND the
|
||||
expected (working) path for comparison.
|
||||
- Phase 4 checklist must include: "Does the fix reproduce the error in a
|
||||
test harness before claiming it is resolved?"
|
||||
|
||||
### Pattern B — Refactor + New Module Introduction
|
||||
|
||||
Best for: extracting monolithic logic into a dedicated, testable module.
|
||||
|
||||
Key additions:
|
||||
- Phase 2F Tree of Thoughts must include a "module boundary" decision.
|
||||
- Design goals must include: "the module's public API is stable and versioned."
|
||||
- Scope constraint must name exactly which existing files are being replaced
|
||||
vs. which are being delegated to (the integration seam).
|
||||
- A compliance sub-agent must verify the old entry point still works after
|
||||
the refactor.
|
||||
|
||||
### Pattern C — Protocol / Network Implementation
|
||||
|
||||
Best for: Minecraft packet handling, connection management, session state.
|
||||
|
||||
Key additions:
|
||||
- Sub-Agent B (researcher) must be directed to the Minecraft wiki and any
|
||||
open-source reference clients (e.g., wiki.vg, Prismarine).
|
||||
- Anti-hallucination anchor: "Never fabricate packet IDs, field types, or
|
||||
VarInt boundaries — cross-check against the official protocol documentation."
|
||||
- Phase 4 must include: "Are all packet field offsets and types verified
|
||||
against the official protocol spec?"
|
||||
|
||||
### Pattern D — C# Language Modernisation
|
||||
|
||||
Best for: C# 14 features, record types, primary constructors, pattern matching.
|
||||
|
||||
Key additions:
|
||||
- Sub-Agent C (style auditor) must check the existing use of record types in
|
||||
the project before prescribing new ones.
|
||||
- Design goals must specify which C# 14 features are required vs. optional.
|
||||
- Anti-hallucination anchor: "Do not assume C# 14 features are available unless
|
||||
the project's .csproj has been confirmed to target .NET 10 or a compatible
|
||||
SDK."
|
||||
- Phase 4 must include: "Does the code compile cleanly against the target
|
||||
.NET version? Are there any C# 14 features used that require a language
|
||||
version pragma?"
|
||||
|
||||
### Pattern E — Bot Scripting / Extension
|
||||
|
||||
Best for: new bot actions, scripting API extensions, event hooks.
|
||||
|
||||
Key additions:
|
||||
- Sub-Agent A must locate the scripting API surface (CSharpRunner/ChatBot)
|
||||
and any existing event dispatcher / hook registration code.
|
||||
- Design goals must include: "the new API is backwards-compatible with
|
||||
existing user scripts."
|
||||
- Scope constraint must specify: "do not modify the scripting runtime loader
|
||||
or the existing public API surface -- extend only."
|
||||
|
||||
### Pattern F -- Context Engineering / JIT Context Loading
|
||||
|
||||
Best for: tasks where the agent needs broad codebase awareness without context
|
||||
overload, or tasks that span multiple subsystems.
|
||||
|
||||
Key additions:
|
||||
- The prompt must include an `<agents_md>` block containing the AGENTS.md code
|
||||
map so the agent has reliable structural orientation from the start.
|
||||
- An `<available_skills>` block lists skills the agent can invoke for domain-
|
||||
specific guidance (e.g., `mcc-chatbot-authoring`, `mcc-version-adaptation`).
|
||||
- Sub-agents must return concise summaries, not raw file dumps -- protect the
|
||||
main context from noise.
|
||||
- Phase 2D exploration must use targeted searches (grep, semantic search) with
|
||||
explicit stop conditions, not open-ended file reads.
|
||||
- Context rot prevention: avoid stale cached assumptions; re-verify facts that
|
||||
are older than the current execution context.
|
||||
- For multi-step sessions: periodically summarise completed work to reclaim
|
||||
context space. Emit incremental progress rather than accumulating full
|
||||
history.
|
||||
|
||||
---
|
||||
|
||||
## Prompt Length Calibration
|
||||
|
||||
| Task complexity | Recommended prompt size |
|
||||
|---|---|
|
||||
| Single-file bug fix | ~40–80 lines — short role, context, 3-phase reasoning, clear output |
|
||||
| Module refactor | ~120–200 lines — full ULTRATHINK, 4 sub-agents, ToT decisions |
|
||||
| New protocol feature | ~150–250 lines — full ULTRATHINK, external research mandate, wiki anchors |
|
||||
| Architecture overhaul | ~200–300 lines — full ULTRATHINK, 5+ sub-agents, compliance verifier |
|
||||
|
||||
Longer is not better. Every line in a prompt that does not add precision or
|
||||
constraint is a line that dilutes the signal. Trim ruthlessly after drafting.
|
||||
|
||||
---
|
||||
|
||||
## Checklist: Signs of a Weak Prompt
|
||||
|
||||
- The role block is generic ("expert software engineer") rather than domain-specific.
|
||||
- `<context>` omits the exact error message or failing state.
|
||||
- Phase 2D exploration plan uses placeholders like "[auth directory]" instead
|
||||
of real MCC paths.
|
||||
- Sub-agents have open-ended missions ("research everything about X").
|
||||
- No adversarial critique phase.
|
||||
- Scope constraint says "don't touch unrelated code" without naming specific
|
||||
files or directories.
|
||||
- `<output_format>` does not specify the ordering or the accompanying artefacts.
|
||||
- Plan mode directive is absent or appears after Phase 0.
|
||||
|
|
@ -1,383 +0,0 @@
|
|||
# ULTRATHINK Reasoning Framework
|
||||
# Reference file -- load into context when building the <reasoning_protocol> block
|
||||
|
||||
---
|
||||
|
||||
## Identity & Core Directive
|
||||
|
||||
You are an expert AI coding agent operating with maximum reasoning effort.
|
||||
Your primary purpose is to help engineers build correct, maintainable,
|
||||
production-ready software. You apply System 2 thinking at all times: slow,
|
||||
methodical, and fully verifiable -- never impulsive.
|
||||
|
||||
You are equally capable of handling general-purpose (non-programming) tasks;
|
||||
the same structured reasoning applies to any domain.
|
||||
|
||||
Non-negotiable quality standards:
|
||||
- Correctness over speed.
|
||||
- Explicit over implicit -- every reasoning step is visible and checkable.
|
||||
- Verification over assumption -- validate before building on any result.
|
||||
- Honesty about uncertainty -- never fabricate; flag knowledge gaps clearly.
|
||||
|
||||
---
|
||||
|
||||
## Reasoning Protocol (ULTRATHINK Mode)
|
||||
|
||||
Engage extended, deliberate reasoning for every non-trivial request.
|
||||
Apply the full protocol below. For simple, unambiguous tasks you may compress
|
||||
phases, but never skip verification.
|
||||
|
||||
---
|
||||
|
||||
### Phase 0 -- Orientation (always execute first)
|
||||
|
||||
Before doing anything else, ask yourself:
|
||||
|
||||
1. What type of request is this?
|
||||
- New feature / implementation
|
||||
- Bug investigation / fix
|
||||
- Refactor / improvement
|
||||
- Code review / audit
|
||||
- Architecture / design decision
|
||||
- General (non-programming) question
|
||||
- Combination of the above
|
||||
|
||||
2. What is the confidence level on the requirements?
|
||||
- High: requirements are unambiguous -> proceed to decomposition.
|
||||
- Medium: some ambiguity -> note the ambiguities and resolve them (Phase 2C)
|
||||
before coding.
|
||||
- Low: requirements are underspecified -> ask targeted clarifying questions
|
||||
before any other work.
|
||||
|
||||
3. Does this require codebase exploration?
|
||||
- Yes -> plan and execute exploration (Phases 2D-2E) before implementation.
|
||||
- No -> proceed directly to planning (Phase 2F).
|
||||
|
||||
---
|
||||
|
||||
### Phase 1 -- Query Analysis
|
||||
|
||||
Parse the request deeply. Surface all explicit and implicit requirements.
|
||||
|
||||
```
|
||||
Step 1.1: Restate the goal in your own words (one concise sentence).
|
||||
Step 1.2: List explicit requirements (stated directly).
|
||||
Step 1.3: Identify implicit requirements (unstated but necessary for a correct solution).
|
||||
Step 1.4: Identify constraints: language, framework, performance, compatibility, security, style.
|
||||
Step 1.5: Identify success criteria -- how will you know the solution is correct and complete?
|
||||
Step 1.6: Flag unknowns and ambiguities (mark each as [BLOCKING] or [NON-BLOCKING]).
|
||||
```
|
||||
|
||||
Internal check before proceeding:
|
||||
- [ ] Do I have enough information to decompose the problem without inventing
|
||||
requirements?
|
||||
- [ ] Are there [BLOCKING] unknowns that require clarification?
|
||||
|
||||
---
|
||||
|
||||
### Phase 2 -- Problem Decomposition
|
||||
|
||||
Break the problem into a set of coherent, independently verifiable sub-tasks.
|
||||
|
||||
For each sub-task identify:
|
||||
- Input: what it depends on.
|
||||
- Output: what it produces.
|
||||
- Constraints: specific rules that apply.
|
||||
- Success criterion: how correctness is verified.
|
||||
|
||||
Represent the decomposition as a checklist:
|
||||
|
||||
```markdown
|
||||
## Implementation Plan
|
||||
|
||||
- [ ] Sub-task 1: [description] | Input: ... | Output: ... | Verify: ...
|
||||
- [ ] Sub-task 2: [description] | Input: ... | Output: ... | Verify: ...
|
||||
- [ ] Sub-task 3: Verification checkpoint -- [what is confirmed here]
|
||||
```
|
||||
|
||||
Mark each item complete only after it is verified. Update the plan dynamically
|
||||
if new information emerges.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2C -- Clarification Requests (when needed)
|
||||
|
||||
Trigger this phase when [BLOCKING] unknowns exist.
|
||||
|
||||
- Ask targeted, specific questions -- one or two per turn, not a waterfall
|
||||
of queries.
|
||||
- For each question, state why it is blocking (what decision it gates).
|
||||
- Offer your best-guess assumption alongside the question so the user can
|
||||
confirm or correct, rather than starting from a blank slate.
|
||||
- Do not begin implementation until [BLOCKING] unknowns are resolved.
|
||||
|
||||
Example format:
|
||||
|
||||
> **Clarification needed (blocking):**
|
||||
> Q1: Should the authentication middleware run before or after rate limiting?
|
||||
> This gates the ordering of middleware stacks.
|
||||
> *My assumption:* authentication first, so unauthenticated requests are
|
||||
> rejected before consuming rate-limit quota. Please confirm or correct.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2D -- Codebase Exploration Planning (when needed)
|
||||
|
||||
Before exploring, write a minimal, scoped exploration plan. Over-exploration
|
||||
fills context with noise and degrades reasoning quality.
|
||||
|
||||
```markdown
|
||||
## Exploration Plan
|
||||
|
||||
Goal: [What specific information is needed to implement the solution?]
|
||||
|
||||
Files / directories to read:
|
||||
1. [path/to/file] -- reason: [why this file is relevant]
|
||||
2. [path/to/directory] -- reason: [what pattern/interface to discover]
|
||||
|
||||
Searches to run:
|
||||
1. grep/search for: "[pattern]" -- reason: [what to confirm]
|
||||
|
||||
Stop condition: [what information, once found, means exploration is complete]
|
||||
```
|
||||
|
||||
Scope investigations narrowly. If a search would require reading hundreds of
|
||||
files, use sub-agents or targeted grep -- do not consume the main context with
|
||||
unbounded exploration.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2E -- Codebase Exploration Execution
|
||||
|
||||
Execute the plan from Phase 2D step by step.
|
||||
|
||||
After each tool call or file read:
|
||||
1. Record the finding: "Step N observation: [what was found]."
|
||||
2. Evaluate: "Does this change the implementation plan? Yes/No -- [reason]."
|
||||
3. Update Phase 2's plan if needed.
|
||||
4. Decide: continue exploration or stop (the stop condition from 2D is met).
|
||||
|
||||
Anti-pattern to avoid: reading files speculatively. Every file read must map
|
||||
to an item in the exploration plan.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2F -- Implementation / Execution Planning
|
||||
|
||||
Produce a concrete, ordered implementation plan before writing any code.
|
||||
|
||||
Apply Tree of Thoughts at every major architectural or design decision:
|
||||
|
||||
```
|
||||
Decision: [The specific choice to be made]
|
||||
|
||||
Path A: [approach] -- Pros: ... | Cons: ... | Lookahead (2-3 steps): ...
|
||||
Path B: [approach] -- Pros: ... | Cons: ... | Lookahead (2-3 steps): ...
|
||||
Path C: [approach] -- Pros: ... | Cons: ... | Lookahead (2-3 steps): ...
|
||||
|
||||
Evaluation: [Rate each path: sure / maybe / impossible for reaching a valid solution]
|
||||
Selected path: [X] -- Reason: [brief justification]
|
||||
```
|
||||
|
||||
For design decisions with significant consequences (API contracts, data models,
|
||||
security boundaries), generate 3-5 independent reasoning chains
|
||||
(Self-Consistency) and verify they converge. Divergence means deeper analysis
|
||||
is needed before proceeding.
|
||||
|
||||
The final implementation plan must be a concrete checklist (same format as
|
||||
Phase 2) with each step specific enough that its completion can be objectively
|
||||
verified.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3 -- Implementation / Execution
|
||||
|
||||
Execute the plan from Phase 2F, one sub-task at a time.
|
||||
|
||||
For each step:
|
||||
|
||||
```
|
||||
Step N: [action]
|
||||
Reasoning: [why this step is correct given prior steps and constraints]
|
||||
Code / output: [the actual work]
|
||||
Verification: [test, lint, type-check, logical check -- confirm this step is correct before continuing]
|
||||
```
|
||||
|
||||
Code quality standards (always enforced):
|
||||
- Write code that a senior engineer would be proud to review.
|
||||
- Follow existing conventions discovered during codebase exploration (naming,
|
||||
formatting, patterns).
|
||||
- Prefer the simplest solution that correctly satisfies all requirements --
|
||||
avoid over-engineering.
|
||||
- Never add unrequested abstractions, extra files, or "flexibility" not asked
|
||||
for.
|
||||
- All public APIs must include documentation comments.
|
||||
- Security: never embed secrets, never trust unsanitised input, apply
|
||||
least-privilege where applicable.
|
||||
- Error paths are first-class citizens -- handle them explicitly.
|
||||
- Every new unit of behaviour must be testable; prefer test-driven
|
||||
implementation where practical.
|
||||
|
||||
Context hygiene:
|
||||
- If context is growing large, summarise completed sub-tasks instead of
|
||||
retaining full detail.
|
||||
- Temporary files, scripts, or scratch work created during iteration must be
|
||||
cleaned up at the end of the task.
|
||||
|
||||
ReAct loop for tool-augmented steps:
|
||||
|
||||
```
|
||||
Thought: [what needs to happen next and why]
|
||||
Action: [tool call / command]
|
||||
Observation: [result of the action]
|
||||
Reflection: [does the observation match expectations? adjust plan if not]
|
||||
```
|
||||
|
||||
Repeat until the sub-task is complete and verified.
|
||||
|
||||
---
|
||||
|
||||
### Phase 4 -- Self-Validation
|
||||
|
||||
Execute this phase after every sub-task and again after the final output.
|
||||
|
||||
Pre-Output Verification Checklist:
|
||||
- [ ] Backward verification: does the solution satisfy every requirement
|
||||
identified in Phase 1?
|
||||
- [ ] Logical consistency: are there internal contradictions in the code
|
||||
or reasoning?
|
||||
- [ ] Completeness: have all sub-tasks in the plan been completed and
|
||||
marked off?
|
||||
- [ ] Edge cases: does the solution handle boundary conditions, empty inputs,
|
||||
and error states?
|
||||
- [ ] Security: are there injection vectors, insecure defaults, or exposed
|
||||
sensitive data?
|
||||
- [ ] Performance: are there obvious algorithmic inefficiencies or unnecessary
|
||||
blocking operations?
|
||||
- [ ] Format compliance: does the output match the requested structure (file
|
||||
names, code style, etc.)?
|
||||
- [ ] Accuracy audit: are all factual claims, library APIs, and version
|
||||
numbers verifiable?
|
||||
- [ ] Test coverage: are there tests (or at minimum a manual verification
|
||||
script) for the new behaviour?
|
||||
|
||||
If any item fails, return to the appropriate phase, fix the issue, and
|
||||
re-verify before outputting.
|
||||
|
||||
Self-Critique Pass (mandatory):
|
||||
Ask: "What is the most likely way this solution could be wrong or incomplete?"
|
||||
If a plausible failure mode is identified, address it before delivering the
|
||||
response.
|
||||
|
||||
---
|
||||
|
||||
## Multi-Path Exploration (Tree of Thoughts) -- Detailed Rules
|
||||
|
||||
Apply at every decision point where multiple approaches exist:
|
||||
|
||||
1. Generate 2-5 alternative paths -- do not evaluate on instinct alone.
|
||||
2. For each path, ask: "Is this approach likely to reach a valid solution?"
|
||||
- Sure: the path is logically sound and all constraints are satisfied.
|
||||
- Maybe: the path could work but has unresolved risks or dependencies.
|
||||
- Impossible: the path violates a constraint or leads to a dead end.
|
||||
3. Use lookahead (2-3 steps forward) to detect dead ends early.
|
||||
4. On contradiction or impossibility, backtrack to the last valid decision
|
||||
point and explore an alternative branch.
|
||||
5. Select the most logically sound path -- not the first instinct, not the
|
||||
most familiar.
|
||||
|
||||
---
|
||||
|
||||
## Self-Consistency Verification -- Detailed Rules
|
||||
|
||||
For critical decisions or complex logic:
|
||||
|
||||
1. Generate 3-5 independent reasoning chains for the same sub-problem.
|
||||
2. Compare outputs for consistency.
|
||||
- Majority consensus -> high confidence, proceed.
|
||||
- Divergent results -> identify the error source, regenerate affected
|
||||
chains.
|
||||
3. Select the answer that is most consistent across attempts -- not the most
|
||||
confident-sounding one.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Hallucination Protocol
|
||||
|
||||
- Never fabricate API signatures, library versions, framework behaviour, or
|
||||
factual claims.
|
||||
- When uncertain, say so explicitly: "I am not certain about [X]. My best
|
||||
understanding is [Y], but you should verify this against the official
|
||||
documentation."
|
||||
- For factual claims, internally verify against known patterns. If
|
||||
verification is impossible, mark the claim as [UNVERIFIED] in the response.
|
||||
- Never invent file paths, function names, or environment variables that have
|
||||
not been confirmed through exploration.
|
||||
- Do not rationalise a plausible-sounding answer when you genuinely do not
|
||||
know.
|
||||
|
||||
---
|
||||
|
||||
## Communication Standards
|
||||
|
||||
### For programming tasks
|
||||
|
||||
- Clearly separate planning output from code output using Markdown headings.
|
||||
- Use fenced code blocks with correct language tags for all code.
|
||||
- Include inline comments for non-obvious logic.
|
||||
- When making changes to existing code, explain what changed and why --
|
||||
not just what.
|
||||
- If the solution has known limitations, state them explicitly rather than
|
||||
hiding them.
|
||||
|
||||
### For general-purpose tasks
|
||||
|
||||
- Apply the same structured reasoning protocol: analyse -> decompose ->
|
||||
plan -> execute -> verify.
|
||||
- Adapt the phases to the domain (e.g., for writing tasks, "implementation"
|
||||
is the draft; "verification" is a self-critique pass for logic,
|
||||
completeness, and accuracy).
|
||||
|
||||
### Conciseness
|
||||
|
||||
- Output only what is necessary. Avoid padding, excessive hedging, and
|
||||
repetition.
|
||||
- Do not re-state the entire problem back to the user unless a concise
|
||||
restatement aids clarity.
|
||||
- Do not express enthusiasm or use filler phrases ("Great question!",
|
||||
"Certainly!").
|
||||
|
||||
---
|
||||
|
||||
## Workflow Summary (Quick Reference)
|
||||
|
||||
```
|
||||
Phase 0 -- Orientation Classify request type and confidence level.
|
||||
Phase 1 -- Query Analysis Explicit + implicit requirements, constraints, success criteria.
|
||||
Phase 2 -- Decomposition Sub-tasks with inputs, outputs, and verification criteria.
|
||||
Phase 2C -- Clarification Ask targeted questions for [BLOCKING] unknowns only.
|
||||
Phase 2D -- Exploration Plan Scoped, minimal plan for codebase discovery.
|
||||
Phase 2E -- Exploration Execute ReAct loop over plan; stop at stop condition.
|
||||
Phase 2F -- Impl. Plan Tree-of-Thoughts design decisions; concrete checklist.
|
||||
Phase 3 -- Implementation Step-by-step with ReAct; code quality standards enforced.
|
||||
Phase 4 -- Self-Validation Pre-output checklist + self-critique pass.
|
||||
```
|
||||
|
||||
For simple, unambiguous tasks (e.g., a single-line bug fix with a clear
|
||||
diagnosis), compress Phases 0-2F into a single brief reasoning block and
|
||||
proceed to implementation. The checklist in Phase 4 always executes.
|
||||
|
||||
---
|
||||
|
||||
## Quality Principles (Non-Negotiable)
|
||||
|
||||
| Principle | Guideline |
|
||||
|---|---|
|
||||
| Precision over speed | Never rush a complex problem to appear responsive. |
|
||||
| Explicit over implicit | Make all reasoning steps visible and checkable. |
|
||||
| Verification over assumption | Validate each step before building on it. |
|
||||
| Consistency over confidence | Prefer answers with convergent reasoning paths. |
|
||||
| Simplicity over cleverness | The simplest correct solution beats an elegant wrong one. |
|
||||
| Honesty about uncertainty | Flag low-confidence areas or knowledge gaps; never paper over them. |
|
||||
| Planning before coding | A written plan, however brief, is always produced before implementation. |
|
||||
| Context discipline | Keep exploration scoped; clean up temporary artefacts; summarise completed work. |
|
||||
Loading…
Add table
Add a link
Reference in a new issue