Skip to content

Commit c0fbde4

Browse files
committed
feat: provide a way to intercept timer callbacks and replace the default ManualTimer
1 parent bf8f680 commit c0fbde4

File tree

6 files changed

+118
-6
lines changed

6 files changed

+118
-6
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Allow `ManualTimeProvider.Start` to be set using property initializers.
1313

14+
- Made the timer type created by `ManualTimeProvider`, the `ManualTimer` type, public, and introduced a protected method `CreateManualTimer` on `ManualTimeProvider`. This enables advanced scenarioes where a custom `ManualTimer` is needed.
15+
16+
A custom implementation of `ManualTimer` can override the `Change` method and add custom behavior to it.
17+
18+
Overriding `CreateManualTimer` makes it possible to intercept a `TimerCallback` and perform actions before and after the timer callback has been invoked.
19+
1420
## [1.0.0-rc.1]
1521

1622
- Updated Microsoft.Bcl.TimeProvider package dependency to rc.1 version.

docs/TimeProviderExtensions.ManualTimeProvider.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,14 +214,51 @@ the expected number of times, i.e. such that the result of `manualTimeProvider.G
214214

215215
Learn more about this behavior at <seealso href="https://github.com/egil/TimeProviderExtensions/#difference-between-manualtimeprovider-and-faketimeprovider"/>.
216216

217+
<a name='TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback,object,TimeProviderExtensions.ManualTimeProvider)'></a>
218+
219+
## ManualTimeProvider.CreateManualTimer(TimerCallback, object, ManualTimeProvider) Method
220+
221+
Creates an instance of a [ManualTimer](TimeProviderExtensions.ManualTimer.md 'TimeProviderExtensions.ManualTimer'). This method is called by [CreateTimer(TimerCallback, object, TimeSpan, TimeSpan)](TimeProviderExtensions.ManualTimeProvider.md#TimeProviderExtensions.ManualTimeProvider.CreateTimer(System.Threading.TimerCallback,object,System.TimeSpan,System.TimeSpan) 'TimeProviderExtensions.ManualTimeProvider.CreateTimer(System.Threading.TimerCallback, object, System.TimeSpan, System.TimeSpan)').
222+
223+
```csharp
224+
protected internal virtual TimeProviderExtensions.ManualTimer CreateManualTimer(System.Threading.TimerCallback callback, object? state, TimeProviderExtensions.ManualTimeProvider timeProvider);
225+
```
226+
#### Parameters
227+
228+
<a name='TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback,object,TimeProviderExtensions.ManualTimeProvider).callback'></a>
229+
230+
`callback` [System.Threading.TimerCallback](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.TimerCallback 'System.Threading.TimerCallback')
231+
232+
A delegate representing a method to be executed when the timer fires. The method specified for callback should be reentrant,
233+
as it may be invoked simultaneously on two threads if the timer fires again before or while a previous callback is still being handled.
234+
235+
<a name='TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback,object,TimeProviderExtensions.ManualTimeProvider).state'></a>
236+
237+
`state` [System.Object](https://docs.microsoft.com/en-us/dotnet/api/System.Object 'System.Object')
238+
239+
An object to be passed to the [callback](TimeProviderExtensions.ManualTimeProvider.md#TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback,object,TimeProviderExtensions.ManualTimeProvider).callback 'TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback, object, TimeProviderExtensions.ManualTimeProvider).callback'). This may be null.
240+
241+
<a name='TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback,object,TimeProviderExtensions.ManualTimeProvider).timeProvider'></a>
242+
243+
`timeProvider` [ManualTimeProvider](TimeProviderExtensions.ManualTimeProvider.md 'TimeProviderExtensions.ManualTimeProvider')
244+
245+
The [ManualTimeProvider](TimeProviderExtensions.ManualTimeProvider.md 'TimeProviderExtensions.ManualTimeProvider') which is used to schedule invocations of the [callback](TimeProviderExtensions.ManualTimeProvider.md#TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback,object,TimeProviderExtensions.ManualTimeProvider).callback 'TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback, object, TimeProviderExtensions.ManualTimeProvider).callback') with.
246+
247+
#### Returns
248+
[ManualTimer](TimeProviderExtensions.ManualTimer.md 'TimeProviderExtensions.ManualTimer')
249+
250+
### Remarks
251+
Override this methods to return a custom implementation of [ManualTimer](TimeProviderExtensions.ManualTimer.md 'TimeProviderExtensions.ManualTimer'). This also allows for intercepting and wrapping
252+
the provided timer [callback](TimeProviderExtensions.ManualTimeProvider.md#TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback,object,TimeProviderExtensions.ManualTimeProvider).callback 'TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback, object, TimeProviderExtensions.ManualTimeProvider).callback') and [state](TimeProviderExtensions.ManualTimeProvider.md#TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback,object,TimeProviderExtensions.ManualTimeProvider).state 'TimeProviderExtensions.ManualTimeProvider.CreateManualTimer(System.Threading.TimerCallback, object, TimeProviderExtensions.ManualTimeProvider).state'), enabling more advanced testing scenarioes.
253+
217254
<a name='TimeProviderExtensions.ManualTimeProvider.CreateTimer(System.Threading.TimerCallback,object,System.TimeSpan,System.TimeSpan)'></a>
218255

219256
## ManualTimeProvider.CreateTimer(TimerCallback, object, TimeSpan, TimeSpan) Method
220257

221258
Creates a new [System.Threading.ITimer](https://docs.microsoft.com/en-us/dotnet/api/System.Threading.ITimer 'System.Threading.ITimer') instance, using [System.TimeSpan](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan 'System.TimeSpan') values to measure time intervals.
222259
223260
```csharp
224-
public override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period);
261+
public sealed override System.Threading.ITimer CreateTimer(System.Threading.TimerCallback callback, object? state, System.TimeSpan dueTime, System.TimeSpan period);
225262
```
226263
#### Parameters
227264

docs/TimeProviderExtensions.ManualTimer.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ The [ManualTimeProvider](TimeProviderExtensions.ManualTimeProvider.md 'TimeProvi
5252

5353
## ManualTimer.CallbackTime Property
5454

55-
Gets the next time the timer callback will be invoked, or `null` if the timer is inactive.
55+
Gets the next time the timer callback will be invoked, or [null](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/null 'https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/null') if the timer is inactive.
5656
5757
```csharp
5858
public System.Nullable<System.DateTimeOffset> CallbackTime { get; }
@@ -74,6 +74,22 @@ public System.TimeSpan DueTime { get; set; }
7474
#### Property Value
7575
[System.TimeSpan](https://docs.microsoft.com/en-us/dotnet/api/System.TimeSpan 'System.TimeSpan')
7676
77+
<a name='TimeProviderExtensions.ManualTimer.IsActive'></a>
78+
79+
## ManualTimer.IsActive Property
80+
81+
Gets whether the timer is currently active, i.e. has a future callback invocation scheduled.
82+
83+
```csharp
84+
public bool IsActive { get; }
85+
```
86+
87+
#### Property Value
88+
[System.Boolean](https://docs.microsoft.com/en-us/dotnet/api/System.Boolean 'System.Boolean')
89+
90+
### Remarks
91+
When [IsActive](TimeProviderExtensions.ManualTimer.md#TimeProviderExtensions.ManualTimer.IsActive 'TimeProviderExtensions.ManualTimer.IsActive') returns [true](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/bool 'https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/bool'), [CallbackTime](TimeProviderExtensions.ManualTimer.md#TimeProviderExtensions.ManualTimer.CallbackTime 'TimeProviderExtensions.ManualTimer.CallbackTime') is not [null](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/null 'https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/null').
92+
7793
<a name='TimeProviderExtensions.ManualTimer.Period'></a>
7894

7995
## ManualTimer.Period Property

src/TimeProviderExtensions/ManualTimeProvider.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,29 @@ public override DateTimeOffset GetUtcNow()
174174
/// To move time forward for the returned <see cref="ITimer"/>, call <see cref="Advance(TimeSpan)"/> or <see cref="SetUtcNow(DateTimeOffset)"/> on this time provider.
175175
/// </para>
176176
/// </remarks>
177-
public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
177+
public sealed override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
178178
{
179-
var result = new ManualTimer(callback, state, this);
179+
var result = CreateManualTimer(callback, state, this);
180180
result.Change(dueTime, period);
181181
return result;
182182
}
183183

184+
/// <summary>
185+
/// Creates an instance of a <see cref="ManualTimer"/>. This method is called by <see cref="CreateTimer(TimerCallback, object?, TimeSpan, TimeSpan)"/>.
186+
/// </summary>
187+
/// <remarks>
188+
/// Override this methods to return a custom implementation of <see cref="ManualTimer"/>. This also allows for intercepting and wrapping
189+
/// the provided timer <paramref name="callback"/> and <paramref name="state"/>, enabling more advanced testing scenarioes.
190+
/// </remarks>
191+
/// <param name="callback">
192+
/// A delegate representing a method to be executed when the timer fires. The method specified for callback should be reentrant,
193+
/// as it may be invoked simultaneously on two threads if the timer fires again before or while a previous callback is still being handled.
194+
/// </param>
195+
/// <param name="state">An object to be passed to the <paramref name="callback"/>. This may be null.</param>
196+
/// <param name="timeProvider">The <see cref="ManualTimeProvider"/> which is used to schedule invocations of the <paramref name="callback"/> with.</param>
197+
protected internal virtual ManualTimer CreateManualTimer(TimerCallback callback, object? state, ManualTimeProvider timeProvider)
198+
=> new ManualTimer(callback, state, timeProvider);
199+
184200
/// <summary>
185201
/// Sets the local time zone.
186202
/// </summary>

src/TimeProviderExtensions/ManualTimer.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Diagnostics;
2+
using System.Diagnostics.CodeAnalysis;
23
using System.Globalization;
34
using System.Runtime.CompilerServices;
45

@@ -16,7 +17,18 @@ public class ManualTimer : ITimer
1617
private ManualTimerScheduler? scheduler;
1718

1819
/// <summary>
19-
/// Gets the next time the timer callback will be invoked, or <c>null</c> if the timer is inactive.
20+
/// Gets whether the timer is currently active, i.e. has a future callback invocation scheduled.
21+
/// </summary>
22+
/// <remarks>
23+
/// When <see cref="IsActive"/> returns <see langword="true"/>, <see cref="CallbackTime"/> is not <see langword="null"/>.
24+
/// </remarks>
25+
#if NET6_0_OR_GREATER
26+
[MemberNotNullWhen(true, nameof(CallbackTime))]
27+
#endif
28+
public bool IsActive => scheduler?.CallbackTime.HasValue ?? false;
29+
30+
/// <summary>
31+
/// Gets the next time the timer callback will be invoked, or <see langword="null"/> if the timer is inactive.
2032
/// </summary>
2133
public DateTimeOffset? CallbackTime => scheduler?.CallbackTime;
2234

test/TimeProviderExtensions.Tests/ManualTimeProviderTests.cs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,5 +281,30 @@ public void ActiveTimers_with_after_timer_state_change()
281281
sut.Advance(1.Seconds());
282282

283283
sut.ActiveTimers.Should().Be(0);
284-
}
284+
}
285+
286+
287+
[Fact]
288+
public void CreateManualTimer_with_custom_timer_type()
289+
{
290+
var sut = new CustomManualTimeProvider();
291+
292+
using var timer = sut.CreateTimer(_ => { }, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
293+
294+
timer.Should().BeOfType<CustomManualTimer>();
295+
}
296+
297+
private sealed class CustomManualTimeProvider : ManualTimeProvider
298+
{
299+
protected internal override ManualTimer CreateManualTimer(TimerCallback callback, object? state, ManualTimeProvider timeProvider)
300+
=> new CustomManualTimer(callback, state, timeProvider);
301+
}
302+
303+
private sealed class CustomManualTimer : ManualTimer
304+
{
305+
internal CustomManualTimer(TimerCallback callback, object? state, ManualTimeProvider timeProvider)
306+
: base(callback, state, timeProvider)
307+
{
308+
}
309+
}
285310
}

0 commit comments

Comments
 (0)