Table of Contents

CryptoHives.Foundation.Threading.Analyzers

Overview

This package provides Roslyn analyzers that detect common ValueTask misuse patterns at compile time. These analyzers help developers avoid subtle bugs and performance issues when working with ValueTask and the pooled async primitives in the CryptoHives Threading library.

Installation

dotnet add package CryptoHives.Foundation.Threading.Analyzers

Or add to your project file:

<PackageReference Include="CryptoHives.Foundation.Threading.Analyzers" Version="*" PrivateAssets="all" />

Diagnostic Rules

ID Severity Description
CHT001 Error ValueTask consumed multiple times (await, AsTask, Preserve, GetResult)
CHT002 Warning ValueTask.GetAwaiter().GetResult() used (blocking)
CHT003 Warning ValueTask stored in field
CHT005 Warning ValueTask.Result accessed directly
CHT007 Info AsTask() stored before signaling (performance)
CHT008 Warning ValueTask not awaited or consumed
CHT009 Info SemaphoreSlim(1, 1) used as async lock; replace with AsyncLock
CHT010 Warning ValueTask captured in lambda/closure
CHT011 Warning async method only forwards an awaited ValueTask
CHT012 Info async ValueTask wrapper boxes a state machine when it suspends

Quick Reference

❌ Anti-Patterns Detected

// CHT001: Multiple await (Error)
ValueTask vt = GetValueTask();
await vt;
await vt; // Error: already consumed

// CHT002: Blocking with GetAwaiter().GetResult() (Warning)
ValueTask vt = GetValueTask();
vt.GetAwaiter().GetResult(); // Warning: undefined behavior

// CHT003: Stored in field (Warning)
private ValueTask _task; // Warning: may be consumed multiple times

// CHT001 also covers repeated AsTask(): converting twice consumes twice (Error)
ValueTask vt = GetValueTask();
var t1 = vt.AsTask();
var t2 = vt.AsTask(); // Error: already consumed

// CHT005: Direct .Result access (Warning)
ValueTask<int> vt = GetValueTask();
int result = vt.Result; // Warning: undefined behavior

// CHT007: AsTask() stored before signaling (Info)
Task t = GetValueTask().AsTask(); // Info: may cause perf degradation
DoSomething();
await t;

// CHT008: Not consumed (Warning)
GetValueTask(); // Warning: result not awaited

// CHT009: SemaphoreSlim(1,1) as async lock (Info)
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1); // Info: consider AsyncLock

✅ Correct Patterns

// Single await - correct
await GetValueTask();

// Store, then single await - correct
ValueTask vt = GetValueTask();
await vt;

// Use Preserve() for safe multiple consumption (built-in on most platforms)
ValueTask vt = GetValueTask();
ValueTask preserved = vt.Preserve();
await preserved;
await preserved; // Safe!

// Use AsTask() once, store Task - correct (all platforms)
Task t = GetValueTask().AsTask();
await t;
await t; // Task can be awaited multiple times

// Pass Task to WhenAll - correct
await Task.WhenAll(
    GetValueTask().AsTask(),
    GetValueTask().AsTask()
);

// Explicit discard - correct
_ = GetValueTask();

Code Fixes

The analyzer package includes automatic code fixes for most diagnostics:

Diagnostic Available Fixes
CHT001 Convert to AsTask() at declaration, Use Preserve()
CHT002 Convert to await, Use AsTask() before GetAwaiter().GetResult()
CHT003 Change field type to Task
CHT005 Convert to await, Use AsTask().Result
CHT007 Await ValueTask directly
CHT008 Add await, Explicitly discard with _ =
CHT009 Replace with AsyncLock
CHT010 Convert to AsTask() at declaration, Use Preserve()
CHT011 Remove async and return the ValueTask directly
CHT012 Pool the state machine box (offered only on .NET 6+; a partial mitigation)

The Preserve() Method

Built-in Preserve()

The ValueTask.Preserve() is available on all platforms.

ValueTask vt = GetValueTask();
ValueTask preserved = vt.Preserve(); // Returns ValueTask that can be awaited multiple times

await preserved;
await preserved; // Safe!

Configuration

Suppressing Diagnostics

You can suppress specific diagnostics using standard methods:

// Pragma directive
#pragma warning disable CHT002
valueTask.GetAwaiter().GetResult();
#pragma warning restore CHT002

// SuppressMessage attribute
[SuppressMessage("Usage", "CHT002")]
public void Method() { }

.editorconfig

Configure rule severity in .editorconfig:

# Make CHT007 a warning instead of info
dotnet_diagnostic.CHT007.severity = warning

# Disable CHT003 entirely
dotnet_diagnostic.CHT003.severity = none

See Also


© 2026 The Keepers of the CryptoHives