CHT003: ValueTask stored in field
Cause
A ValueTask or ValueTask<T> is stored in a field or auto-property.
Rule Description
Storing a ValueTask in a field creates a high risk of consuming it multiple times. Fields persist beyond the scope of a single method, making it easy to accidentally:
- Await the stored
ValueTaskmultiple times - Access the
ValueTaskfrom multiple threads - Forget that the
ValueTaskwas already consumed
A ValueTask should be consumed immediately or stored temporarily in a local variable within a single method scope.
How to Fix
Option 1: Store as Task
Change the field type to Task and use AsTask():
// Before
private ValueTask _pendingWork;
public async Task StartAsync()
{
_pendingWork = DoWorkAsync();
}
// After
private Task? _pendingWork;
public async Task StartAsync()
{
_pendingWork = DoWorkAsync().AsTask();
}
Option 2: Store the result
If the operation is already completed, store the result instead:
// Before
private ValueTask<int> _cachedValue;
// After
private int _cachedValue;
private bool _hasValue;
Option 3: Use Preserve()
private Task? _pendingWork;
public async Task StartAsync()
{
_pendingWork = DoWorkAsync().Preserve();
}
When to Suppress
Suppress only if you have a very specific use case where:
- The field is only accessed from a single code path
- You guarantee single consumption
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CHT003")]
private ValueTask _singleUseTask;
Example
Violating Code
public class MyService
{
private ValueTask _initTask; // CHT003
public void Initialize()
{
_initTask = InitializeAsync();
}
public async Task WaitForInitAsync()
{
await _initTask; // Might be called multiple times!
}
}
Fixed Code
public class MyService
{
private Task? _initTask;
public void Initialize()
{
_initTask = InitializeAsync().AsTask();
}
public async Task WaitForInitAsync()
{
if (_initTask != null)
{
await _initTask; // Safe - Task can be awaited multiple times
}
}
}