.NET 8 finally introduced a time abstraction that can be used to fake advance a clock while testing certain components.
https://learn.microsoft.com/en-us/dotnet/api/system.timeprovider?view=net-9.0
The code below should behave exactly like Task.Delay and should be a 1:1 replacement.
- Is this code fully non blocking in all cases?
- Are there any other issues with this code?
public static class TimeProviderExtensions
{
/// <summary>
/// Creates a task that completes after the specified time delay using the given TimeProvider.
/// </summary>
/// <param name="timeProvider">The TimeProvider to schedule the timer with.</param>
/// <param name="delay">The time to wait before completing the returned task, or Timeout.InfiniteTimeSpan.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the delay.</param>
/// <returns>A task that represents the time delay.</returns>
/// <exception cref="ArgumentOutOfRangeException">
/// Thrown if <paramref name="delay"/> is less than -1 milliseconds and not Timeout.InfiniteTimeSpan.
/// </exception>
public static Task Delay(this TimeProvider timeProvider, TimeSpan delay, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(timeProvider);
// Validate the delay argument similar to how Task.Delay would
if (delay < TimeSpan.Zero && delay != Timeout.InfiniteTimeSpan)
throw new ArgumentOutOfRangeException(nameof(delay), "Delay must be non-negative or Timeout.InfiniteTimeSpan.");
// If token is already canceled, return a canceled task
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled(cancellationToken);
// If no delay is needed, return a completed task
if (delay == TimeSpan.Zero)
return Task.CompletedTask;
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
CancellationTokenRegistration ctr = default;
ITimer? timer = null;
// Set up cancellation if requested
if (cancellationToken.CanBeCanceled)
{
ctr = cancellationToken.Register(() =>
{
timer?.Dispose();
tcs.SetCanceled(cancellationToken);
});
}
// Create a one-shot timer that completes the TCS when elapsed
timer = timeProvider.CreateTimer(_ =>
{
// Timer has fired, dispose cancellation registration and complete the task
ctr.Dispose();
tcs.TrySetResult();
}, state: null, dueTime: delay, period: Timeout.InfiniteTimeSpan);
return tcs.Task;
}
}