Skip to content

Commit ed5ea56

Browse files
committed
feat: Added TimeProvider.CreateCancellationTokenSource(TimeSpan delay)
1 parent f7738fd commit ed5ea56

File tree

4 files changed

+69
-6
lines changed

4 files changed

+69
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
## [0.8.0]
1111

1212
- Added `TimeProvider.GetElapsedTime(long startingTimestamp)`
13+
- Added `TimeProvider.CreateCancellationTokenSource(TimeSpan delay)`
1314

1415
## [0.7.0]
1516

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ Currently, the following .NET time-based APIs are supported:
1010
|----------------------|----------------------|
1111
| `GetUtcNow()` method | `DateTimeOffset.UtcNow` property |
1212
| `CreateTimer()` method | `System.Threading.Timer` type |
13-
| `CreatePeriodicTimer(TimeSpan)` method (only .NET 6) | `System.Threading.PeriodicTimer` type |
13+
| `TimeProvider.CreatePeriodicTimer(TimeSpan)` method (only .NET 6) | `System.Threading.PeriodicTimer` type |
1414
| `Delay(TimeSpan, CancellationToken)` method | `Task.Delay(TimeSpan, CancellationToken)` method |
15-
| `CancellationTokenSource.CancelAfter(TimeSpan, TimeProvider)` method | `CancellationTokenSource.CancelAfter(TimeSpan)` method |
15+
| `CancellationTokenSource.CancelAfter(TimeSpan, TimeProvider)` and `TimeProvider.CreateCancellationTokenSource(TimeSpan delay)` methods | `CancellationTokenSource.CancelAfter(TimeSpan)` method |
1616
| `Task.WaitAsync(TimeSpan, TimeProvider)` method (only .NET 6)| `Task.WaitAsync(TimeSpan)` method |
1717
| `Task.WaitAsync(TimeSpan, TimeProvider, CancellationToken)` method (only .NET 6)| `Task.WaitAsync(TimeSpan, CancellationToken)` method |
1818

src/TimeScheduler/System/Threading/TimeProviderCancellationTokenSourceExtensions.cs

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,69 @@ public static class TimeProviderCancellationTokenSourceExtensions
1111
private const uint UnsignedInfinite = unchecked((uint)-1);
1212
private static readonly ConditionalWeakTable<CancellationTokenSource, ITimer> CancellationTokenSourceTimerMap = new();
1313

14+
/// <summary>Initializes a new instance of the <see cref="CancellationTokenSource"/> class that will be canceled after the specified <see cref="TimeSpan"/>. </summary>
15+
/// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret the <paramref name="delay"/>. </param>
16+
/// <param name="delay">The time interval to wait before canceling this <see cref="CancellationTokenSource"/>. </param>
17+
/// <exception cref="ArgumentOutOfRangeException"> The <paramref name="delay"/> is negative and not equal to <see cref="Timeout.InfiniteTimeSpan" /> or greater than maximum allowed timer duration.</exception>
18+
/// <returns><see cref="CancellationTokenSource"/> that will be canceled after the specified <paramref name="delay"/>.</returns>
19+
/// <remarks>
20+
/// <para>
21+
/// The countdown for the delay starts during the call to the constructor. When the delay expires,
22+
/// the constructed <see cref="CancellationTokenSource"/> is canceled if it has
23+
/// not been canceled already.
24+
/// </para>
25+
/// <para>
26+
/// If running on .NET versions earlier than .NET 8.0, there is a constraint when invoking <see cref="CancellationTokenSource.CancelAfter(TimeSpan)"/> on the resultant object.
27+
/// This action will not terminate the initial timer indicated by <paramref name="delay"/>. However, this restriction does not apply on .NET 8.0 and later versions.
28+
/// </para>
29+
/// </remarks>
30+
public static CancellationTokenSource CreateCancellationTokenSource(this TimeProvider timeProvider, TimeSpan delay)
31+
{
32+
#if NET8_0_OR_GREATER
33+
return new CancellationTokenSource(delay, timeProvider);
34+
#else
35+
if (timeProvider is null)
36+
{
37+
throw new ArgumentNullException(nameof(timeProvider));
38+
}
39+
40+
if (delay != Timeout.InfiniteTimeSpan && delay < TimeSpan.Zero)
41+
{
42+
throw new ArgumentOutOfRangeException(nameof(delay));
43+
}
44+
45+
if (timeProvider == TimeProvider.System)
46+
{
47+
return new CancellationTokenSource(delay);
48+
}
49+
50+
var cts = new CancellationTokenSource();
51+
52+
ITimer timer = timeProvider.CreateTimer(s =>
53+
{
54+
try
55+
{
56+
((CancellationTokenSource)s!).Cancel();
57+
}
58+
catch (ObjectDisposedException) { }
59+
}, cts, delay, Timeout.InfiniteTimeSpan);
60+
61+
cts.Token.Register(t => ((ITimer)t!).Dispose(), timer);
62+
return cts;
63+
#endif // NET8_0_OR_GREATER
64+
}
65+
1466
/// <summary>
1567
/// Schedules a Cancel operation on the <paramref name="cancellationTokenSource"/>.
1668
/// </summary>
17-
/// <param name="timeProvider">
18-
/// The <see cref="TimeProvider"/> to use for scheduling the cancellation.
19-
/// </param>
2069
/// <param name="cancellationTokenSource">
2170
/// The <see cref="CancellationTokenSource"/> to cancel after the specified delay.
2271
/// </param>
2372
/// <param name="delay">The time span to wait before canceling the <paramref name="cancellationTokenSource"/>.
2473
/// </param>
74+
/// <param name="timeProvider">
75+
/// The <see cref="TimeProvider"/> to use for scheduling the cancellation.
76+
/// </param>
2577
/// <exception cref="ObjectDisposedException">The exception thrown when the <paramref name="cancellationTokenSource"/>
2678
/// has been disposed.
2779
/// </exception>
@@ -32,7 +84,7 @@ public static class TimeProviderCancellationTokenSourceExtensions
3284
/// <remarks>
3385
/// <para>
3486
/// The countdown for the delay starts during this call. When the delay expires,
35-
/// the <paramref name="cancellationTokenSource"/> is canceled, if it has
87+
/// the <paramref name="cancellationTokenSource"/> is cancel ed, if it has
3688
/// not been canceled already.
3789
/// </para>
3890
/// <para>

test/TimeScheduler.Tests/System/TimeProviderTests.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,16 @@ public async Task CancelAfter_cancels()
260260
await throwsAfterCancel.Should().ThrowAsync<TaskCanceledException>();
261261
}
262262

263+
[Fact]
264+
public async Task CreateCancellationTokenSource_cancels()
265+
{
266+
var delay = TimeSpan.FromMilliseconds(30);
267+
using var cts = TimeProvider.System.CreateCancellationTokenSource(delay);
268+
269+
var throwsAfterCancel = () => Task.Delay(TimeSpan.FromHours(1), cts.Token);
270+
await throwsAfterCancel.Should().ThrowAsync<TaskCanceledException>();
271+
}
272+
263273
[Fact]
264274
public async Task CancelAfter_reschedule_cancel()
265275
{

0 commit comments

Comments
 (0)