Skip to content

Commit 738ab3d

Browse files
committed
docs: add code docs as markdown
1 parent bb5875d commit 738ab3d

8 files changed

+747
-40
lines changed

README.md

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -17,47 +17,24 @@ This describes how to get started:
1717

1818
2. Take a dependency on `TimeProvider` in your code. Inject the production version of `TimeProvider` available via the [`TimeProvider.System`](https://learn.microsoft.com/en-us/dotnet/api/system.timeprovider.system?#system-timeprovider-system) property during production.
1919

20-
3. During testing, inject the `ManualTimeProvider` from this library. With `ManualTimeProvider`, you can move advance time by calling `Advance(TimeSpan)` or `SetUtcNow(DateTimeOffset)` and jump ahead in time using `Jump(TimeSpan)` or `Jump(DateTimeOffset)`. This allows you to write tests that run fast and predictably, even if the system under test pauses execution for multiple minutes using e.g. `TimeProvider.Delay(TimeSpan)`, the replacement for `Task.Delay(TimeSpan)`.
20+
3. During testing, inject the `ManualTimeProvider` from this library. This allows you to write tests that run fast and predictably.
21+
- Advance time by calling `Advance(TimeSpan)` or `SetUtcNow(DateTimeOffset)` or
22+
- Jump ahead in time using `Jump(TimeSpan)` or `Jump(DateTimeOffset)`
2123

22-
Read the rest of this README for further details and examples.
24+
4. See the **[`ManualTimeProvider API`](https://github.com/egil/TimeProviderExtensions/blob/main/docs/TimeProviderExtensions.ManualTimeProvider.md) page** for the full API documentation for `ManualTimeProvider`.
2325

24-
## ManualTimeProvider API
26+
5. Read the rest of this README for further details and examples.
2527

26-
The ManualTimeProvider represents a synthetic time provider that can be used to enable deterministic behavior in tests.
28+
## API Overview
2729

28-
## Difference between `ManualTimeProvider` and `FakeTimeProvider`
29-
30-
The .NET team has published a similar test-specific time provider, the [`Microsoft.Extensions.Time.Testing.FakeTimeProvider`](https://www.nuget.org/packages/Microsoft.Extensions.Time.Testing.FakeTimeProvider/).
31-
32-
The public API of both `FakeTimeProvider` and `ManualTimeProvider` are compatible, but there are some differences in when time is set before timer callbacks. Let's illustrate this with an example:
33-
34-
For example, if we create an `ITimer` with a *due time* and *period* set to **1 second**, the `DateTimeOffset` returned from `GetUtcNow()` during the timer callback may be different depending on the amount passed to `Advance()` (or `SetUtcNow()`).
35-
36-
If we call `Advance(TimeSpan.FromSeconds(1))` three times, effectively moving time forward by three seconds, the timer callback will be invoked once at times `00:01`, `00:02`, and `00:03`, as illustrated in the drawing below. Both `FakeTimeProvider` and `ManualTimeProvider` behave like this:
37-
38-
![Advancing time by three seconds in one-second increments.](https://raw.githubusercontent.com/egil/TimeProviderExtensions/main/docs/advance-1-second.svg)
39-
40-
If we instead call `Advance(TimeSpan.FromSeconds(3))` once, the two implementations behave differently. `ManualTimeProvider` will invoke the timer callback at the same time (`00:01`, `00:02`, and `00:03`) as if we had called `Advance(TimeSpan.FromSeconds(1))` three times, as illustrated in the drawing below:
41-
42-
![Advancing time by three seconds in one step using ManualTimeProvider.](https://raw.githubusercontent.com/egil/TimeProviderExtensions/main/docs/ManualTimeProvider-advance-3-seconds.svg)
43-
44-
However, `FakeTimeProvider` will invoke the timer callback at time `00:03` three times, as illustrated in the drawings below:
45-
46-
![Advancing time by three seconds in one step using FakeTimeProvider.](https://raw.githubusercontent.com/egil/TimeProviderExtensions/main/docs/FakeTimeProvider-advance-3-seconds.svg)
47-
48-
Technically, both implementations are correct since the `ITimer` abstractions only promise to invoke the callback timer *on or after the due time/period has elapsed*, never before.
49-
50-
However, I strongly prefer the `ManualTimeProvider` approach since it behaves consistently independent of how time is moved forward. It seems much more in the spirit of how a deterministic time provider should behave and avoids users being surprised when writing tests. I imagine users may get stuck for a while trying to debug why the time reported by `GetUtcNow()` is not set as expected due to the subtle difference in the behavior of `FakeTimeProvider`.
51-
52-
That said, it can be useful to test that your code behaves correctly if a timer isn't allocated processor time immediately when it's callback should fire, and for that, `ManualTimeProvider` includes a different method, `Jump`.
53-
54-
### Jumping to a point in time
30+
These pages has all the details of the API included in this package:
5531

56-
A real `ITimer`'s callback may not be allocated processor time and be able to fire at the moment it has been scheduled, e.g. if the processor is busy doing other things. The callback will eventually fire (unless the timer is disposed of).
32+
- [`ManualTimeProvider`](https://github.com/egil/TimeProviderExtensions/blob/main/docs/TimeProviderExtensions.ManualTimeProvider.md)
5733

58-
To support testing this scenario, `ManualtTimeProvider` includes a method that will jump time to a specific point, and then invoke all scheduled timer callbacks between the start and end of the jump. This behavior is similar to how `FakeTimeProvider`s `Advance` method works, as described in the previous section.
34+
**.NET 7 and earlier:**
5935

60-
![Jumping ahead in time by three seconds in one step using ManualTimeProvider.](https://raw.githubusercontent.com/egil/TimeProviderExtensions/main/docs/jump-3-seconds.svg)
36+
- [`PeriodicTimerWrapper`](https://github.com/egil/TimeProviderExtensions/blob/main/docs/System.Threading.PeriodicTimerWrapper.md)
37+
- [`TimeProviderPeriodicTimerExtensions`](https://github.com/egil/TimeProviderExtensions/blob/main/docs/System.Threading.TimeProviderPeriodicTimerExtensions.md)
6138

6239
## Known limitations and issues:
6340

@@ -216,3 +193,37 @@ public async Task DoStuff_does_stuff_every_11_seconds()
216193
.BeCloseTo(DateTimeOffset.UtcNow, precision: TimeSpan.FromMilliseconds(50));
217194
}
218195
```
196+
197+
## Difference between `ManualTimeProvider` and `FakeTimeProvider`
198+
199+
The .NET team has published a similar test-specific time provider, the [`Microsoft.Extensions.Time.Testing.FakeTimeProvider`](https://www.nuget.org/packages/Microsoft.Extensions.Time.Testing.FakeTimeProvider/).
200+
201+
The public API of both `FakeTimeProvider` and `ManualTimeProvider` are compatible, but there are some differences in when time is set before timer callbacks. Let's illustrate this with an example:
202+
203+
For example, if we create an `ITimer` with a *due time* and *period* set to **1 second**, the `DateTimeOffset` returned from `GetUtcNow()` during the timer callback may be different depending on the amount passed to `Advance()` (or `SetUtcNow()`).
204+
205+
If we call `Advance(TimeSpan.FromSeconds(1))` three times, effectively moving time forward by three seconds, the timer callback will be invoked once at times `00:01`, `00:02`, and `00:03`, as illustrated in the drawing below. Both `FakeTimeProvider` and `ManualTimeProvider` behave like this:
206+
207+
![Advancing time by three seconds in one-second increments.](https://raw.githubusercontent.com/egil/TimeProviderExtensions/main/docs/advance-1-second.svg)
208+
209+
If we instead call `Advance(TimeSpan.FromSeconds(3))` once, the two implementations behave differently. `ManualTimeProvider` will invoke the timer callback at the same time (`00:01`, `00:02`, and `00:03`) as if we had called `Advance(TimeSpan.FromSeconds(1))` three times, as illustrated in the drawing below:
210+
211+
![Advancing time by three seconds in one step using ManualTimeProvider.](https://raw.githubusercontent.com/egil/TimeProviderExtensions/main/docs/ManualTimeProvider-advance-3-seconds.svg)
212+
213+
However, `FakeTimeProvider` will invoke the timer callback at time `00:03` three times, as illustrated in the drawings below:
214+
215+
![Advancing time by three seconds in one step using FakeTimeProvider.](https://raw.githubusercontent.com/egil/TimeProviderExtensions/main/docs/FakeTimeProvider-advance-3-seconds.svg)
216+
217+
Technically, both implementations are correct since the `ITimer` abstractions only promise to invoke the callback timer *on or after the due time/period has elapsed*, never before.
218+
219+
However, I strongly prefer the `ManualTimeProvider` approach since it behaves consistently independent of how time is moved forward. It seems much more in the spirit of how a deterministic time provider should behave and avoids users being surprised when writing tests. I imagine users may get stuck for a while trying to debug why the time reported by `GetUtcNow()` is not set as expected due to the subtle difference in the behavior of `FakeTimeProvider`.
220+
221+
That said, it can be useful to test that your code behaves correctly if a timer isn't allocated processor time immediately when it's callback should fire, and for that, `ManualTimeProvider` includes a different method, `Jump`.
222+
223+
### Jumping to a point in time
224+
225+
A real `ITimer`'s callback may not be allocated processor time and be able to fire at the moment it has been scheduled, e.g. if the processor is busy doing other things. The callback will eventually fire (unless the timer is disposed of).
226+
227+
To support testing this scenario, `ManualtTimeProvider` includes a method that will jump time to a specific point, and then invoke all scheduled timer callbacks between the start and end of the jump. This behavior is similar to how `FakeTimeProvider`s `Advance` method works, as described in the previous section.
228+
229+
![Jumping ahead in time by three seconds in one step using ManualTimeProvider.](https://raw.githubusercontent.com/egil/TimeProviderExtensions/main/docs/jump-3-seconds.svg)
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#### [TimeProviderExtensions](index.md 'index')
2+
### [System.Threading](index.md#System.Threading 'System.Threading')
3+
4+
## PeriodicTimerWrapper Class
5+
6+
Provides a lightweight wrapper around a [System.Threading.PeriodicTimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer 'System.Threading.PeriodicTimer') to enable controlling the timer via a [System.TimeProvider](https://docs.microsoft.com/en-us/dotnet/api/System.TimeProvider 'System.TimeProvider').
7+
A periodic timer enables waiting asynchronously for timer ticks.
8+
9+
```csharp
10+
public abstract class PeriodicTimerWrapper :
11+
System.IDisposable
12+
```
13+
14+
Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') 🡒 PeriodicTimerWrapper
15+
16+
Implements [System.IDisposable](https://docs.microsoft.com/en-us/dotnet/api/System.IDisposable 'System.IDisposable')
17+
18+
### Remarks
19+
20+
This timer is intended to be used only by a single consumer at a time: only one call to [WaitForNextTickAsync(CancellationToken)](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken) 'System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken)')
21+
may be in flight at any given moment. [Dispose()](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.Dispose() 'System.Threading.PeriodicTimerWrapper.Dispose()') may be used concurrently with an active [WaitForNextTickAsync(CancellationToken)](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken) 'System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken)')
22+
to interrupt it and cause it to return false.
23+
### Methods
24+
25+
<a name='System.Threading.PeriodicTimerWrapper.~PeriodicTimerWrapper()'></a>
26+
27+
## PeriodicTimerWrapper.~PeriodicTimerWrapper() Method
28+
29+
Ensures that resources are freed and other cleanup operations are performed when the garbage collector reclaims the [System.Threading.PeriodicTimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer 'System.Threading.PeriodicTimer') object.
30+
31+
```csharp
32+
~PeriodicTimerWrapper();
33+
```
34+
35+
<a name='System.Threading.PeriodicTimerWrapper.Dispose()'></a>
36+
37+
## PeriodicTimerWrapper.Dispose() Method
38+
39+
Stops the timer and releases associated managed resources.
40+
41+
```csharp
42+
public void Dispose();
43+
```
44+
45+
Implements [Dispose()](https://docs.microsoft.com/en-us/dotnet/api/System.IDisposable.Dispose 'System.IDisposable.Dispose')
46+
47+
### Remarks
48+
[Dispose()](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.Dispose() 'System.Threading.PeriodicTimerWrapper.Dispose()') will cause an active wait with [WaitForNextTickAsync(CancellationToken)](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken) 'System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken)') to complete with a value of false.
49+
All subsequent [WaitForNextTickAsync(CancellationToken)](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken) 'System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken)') invocations will produce a value of false.
50+
51+
<a name='System.Threading.PeriodicTimerWrapper.Dispose(bool)'></a>
52+
53+
## PeriodicTimerWrapper.Dispose(bool) Method
54+
55+
Dispose of the wrapped [System.Threading.PeriodicTimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer 'System.Threading.PeriodicTimer').
56+
57+
```csharp
58+
protected abstract void Dispose(bool disposing);
59+
```
60+
#### Parameters
61+
62+
<a name='System.Threading.PeriodicTimerWrapper.Dispose(bool).disposing'></a>
63+
64+
`disposing` [System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean')
65+
66+
<a name='System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken)'></a>
67+
68+
## PeriodicTimerWrapper.WaitForNextTickAsync(CancellationToken) Method
69+
70+
Wait for the next tick of the timer, or for the timer to be stopped.
71+
72+
```csharp
73+
public abstract System.Threading.Tasks.ValueTask<bool> WaitForNextTickAsync(System.Threading.CancellationToken cancellationToken=default(System.Threading.CancellationToken));
74+
```
75+
#### Parameters
76+
77+
<a name='System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken).cancellationToken'></a>
78+
79+
`cancellationToken` [System.Threading.CancellationToken](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.CancellationToken 'System.Threading.CancellationToken')
80+
81+
A [System.Threading.CancellationToken](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.CancellationToken 'System.Threading.CancellationToken') to use to cancel the asynchronous wait. If cancellation is requested, it affects only the single wait operation;
82+
the underlying timer continues firing.
83+
84+
#### Returns
85+
[System.Threading.Tasks.ValueTask&lt;](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.Tasks.ValueTask-1 'System.Threading.Tasks.ValueTask`1')[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean')[&gt;](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.Tasks.ValueTask-1 'System.Threading.Tasks.ValueTask`1')
86+
A task that will be completed due to the timer firing, [Dispose()](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.Dispose() 'System.Threading.PeriodicTimerWrapper.Dispose()') being called to stop the timer, or cancellation being requested.
87+
88+
### Remarks
89+
The [System.Threading.PeriodicTimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer 'System.Threading.PeriodicTimer') behaves like an auto-reset event, in that multiple ticks are coalesced into a single tick if they occur between
90+
calls to [WaitForNextTickAsync(CancellationToken)](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken) 'System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken)'). Similarly, a call to [Dispose()](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.Dispose() 'System.Threading.PeriodicTimerWrapper.Dispose()') will void any tick not yet consumed. [WaitForNextTickAsync(CancellationToken)](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken) 'System.Threading.PeriodicTimerWrapper.WaitForNextTickAsync(System.Threading.CancellationToken)')
91+
may only be used by one consumer at a time, and may be used concurrently with a single call to [Dispose()](System.Threading.PeriodicTimerWrapper.md#System.Threading.PeriodicTimerWrapper.Dispose() 'System.Threading.PeriodicTimerWrapper.Dispose()').
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#### [TimeProviderExtensions](index.md 'index')
2+
### [System.Threading](index.md#System.Threading 'System.Threading')
3+
4+
## TimeProviderPeriodicTimerExtensions Class
5+
6+
PeriodicTimer extensions for [System.TimeProvider](https://docs.microsoft.com/en-us/dotnet/api/System.TimeProvider 'System.TimeProvider').
7+
8+
```csharp
9+
public static class TimeProviderPeriodicTimerExtensions
10+
```
11+
12+
Inheritance [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object') &#129106; TimeProviderPeriodicTimerExtensions
13+
### Methods
14+
15+
<a name='System.Threading.TimeProviderPeriodicTimerExtensions.CreatePeriodicTimer(thisSystem.TimeProvider,System.TimeSpan)'></a>
16+
17+
## TimeProviderPeriodicTimerExtensions.CreatePeriodicTimer(this TimeProvider, TimeSpan) Method
18+
19+
Factory method that creates a periodic timer that enables waiting asynchronously for timer ticks.
20+
Use this factory method as a replacement for instantiating a [System.Threading.PeriodicTimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer 'System.Threading.PeriodicTimer').
21+
22+
```csharp
23+
public static System.Threading.PeriodicTimerWrapper CreatePeriodicTimer(this System.TimeProvider timeProvider, System.TimeSpan period);
24+
```
25+
#### Parameters
26+
27+
<a name='System.Threading.TimeProviderPeriodicTimerExtensions.CreatePeriodicTimer(thisSystem.TimeProvider,System.TimeSpan).timeProvider'></a>
28+
29+
`timeProvider` [System.TimeProvider](https://docs.microsoft.com/en-us/dotnet/api/System.TimeProvider 'System.TimeProvider')
30+
31+
<a name='System.Threading.TimeProviderPeriodicTimerExtensions.CreatePeriodicTimer(thisSystem.TimeProvider,System.TimeSpan).period'></a>
32+
33+
`period` [System.TimeSpan](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan 'System.TimeSpan')
34+
35+
#### Returns
36+
[PeriodicTimerWrapper](System.Threading.PeriodicTimerWrapper.md 'System.Threading.PeriodicTimerWrapper')
37+
A new [PeriodicTimerWrapper](System.Threading.PeriodicTimerWrapper.md 'System.Threading.PeriodicTimerWrapper').
38+
Note, this is a wrapper around a [System.Threading.PeriodicTimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer 'System.Threading.PeriodicTimer'),
39+
and will behave exactly the same as the original.
40+
41+
### Remarks
42+
This timer is intended to be used only by a single consumer at a time: only one call to [System.Threading.PeriodicTimer.WaitForNextTickAsync(System.Threading.CancellationToken)](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer.WaitForNextTickAsync#System_Threading_PeriodicTimer_WaitForNextTickAsync_System_Threading_CancellationToken_ 'System.Threading.PeriodicTimer.WaitForNextTickAsync(System.Threading.CancellationToken)')
43+
may be in flight at any given moment. [System.Threading.PeriodicTimer.Dispose](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer.Dispose 'System.Threading.PeriodicTimer.Dispose') may be used concurrently with an active [System.Threading.PeriodicTimer.WaitForNextTickAsync(System.Threading.CancellationToken)](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.PeriodicTimer.WaitForNextTickAsync#System_Threading_PeriodicTimer_WaitForNextTickAsync_System_Threading_CancellationToken_ 'System.Threading.PeriodicTimer.WaitForNextTickAsync(System.Threading.CancellationToken)')
44+
to interrupt it and cause it to return false.

0 commit comments

Comments
 (0)