TimeProviderExtensions 1.0.0
dotnet add package TimeProviderExtensions --version 1.0.0
NuGet\Install-Package TimeProviderExtensions -Version 1.0.0
<PackageReference Include="TimeProviderExtensions" Version="1.0.0" />
paket add TimeProviderExtensions --version 1.0.0
#r "nuget: TimeProviderExtensions, 1.0.0"
// Install TimeProviderExtensions as a Cake Addin #addin nuget:?package=TimeProviderExtensions&version=1.0.0 // Install TimeProviderExtensions as a Cake Tool #tool nuget:?package=TimeProviderExtensions&version=1.0.0
TimeProvider Extensions
Testing extensions for the System.TimeProvider
API. It includes:
- An advanced test/fake version of the
TimeProvider
type, namedManualTimeProvider
, that allows you to control the progress of time during testing deterministically (see the difference to Microsoft'sFakeTimeProvider
below). - A backported version of
PeriodicTimer
that supportsTimeProvider
in .NET 6.
Quick start
This describes how to get started:
Get the latest release from https://www.nuget.org/packages/TimeProviderExtensions.
Take a dependency on
TimeProvider
in your production code. Inject the production version ofTimeProvider
available via theTimeProvider.System
property during production.During testing, inject the
ManualTimeProvider
from this library. This allows you to write tests that run fast and predictably.- Advance time by calling
Advance(TimeSpan)
orSetUtcNow(DateTimeOffset)
or - Jump ahead in time using
Jump(TimeSpan)
orJump(DateTimeOffset)
.
- Advance time by calling
See the
ManualTimeProvider API
page for the full API documentation forManualTimeProvider
.Read the rest of this README for further details and examples.
API Overview
These pages have all the details of the API included in this package:
.NET 7 and earlier:
Known limitations and issues:
- If running on .NET versions earlier than .NET 8.0, there is a constraint when invoking
CancellationTokenSource.CancelAfter(TimeSpan)
on theCancellationTokenSource
object returned byCreateCancellationTokenSource(TimeSpan delay)
. This action will not terminate the initial timer indicated by thedelay
argument initially passed theCreateCancellationTokenSource
method. However, this restriction does not apply to .NET 8.0 and later versions. - To enable controlling
PeriodicTimer
viaTimeProvider
in versions of .NET earlier than .NET 8.0, theTimeProvider.CreatePeriodicTimer
returns aPeriodicTimerWrapper
object instead of aPeriodicTimer
object. ThePeriodicTimerWrapper
type is just a lightweight wrapper around the originalSystem.Threading.PeriodicTimer
and will behave identically to it. - If
ManualTimeProvider
is created via AutoFixture, be aware that will set writable properties with random values. This behavior can be overridden by providing a customization to AutoFixture, e.g.:
or by using thefixture.Customize<ManualTimeProvider>(x => x.OmitAutoProperties()));
[NoAutoProperties]
attribute, if usingAutoFixture.Xunit2
.
Installation and Usage
Get the latest release from https://www.nuget.org/packages/TimeProviderExtensions
Set up in production
To use in production, pass in TimeProvider.System
to the types that depend on TimeProvider
.
This can be done directly or via an IoC Container, e.g., .NETs built-in IServiceCollection
like so:
services.AddSingleton(TimeProvider.System);
If you do not want to register the TimeProvider
with your IoC container, you can instead create
an additional constructor in the types that use it, which allows you to pass in a TimeProvider
,
and in the existing constructor(s) you have, just new up TimeProvider.System
directly. For example:
public class MyService
{
private readonly TimeProvider timeProvider;
public MyService() : this(TimeProvider.System)
{
}
public MyService(TimeProvider timeProvider)
{
this.timeProvider = timeProvider;
}
}
This allows you to explicitly pass in a ManualTimeProvider
during testing.
Example - control time during tests
If a system under test (SUT) uses things like Task.Delay
, DateTimeOffset.UtcNow
, Task.WaitAsync
, or PeriodicTimer
,
it becomes hard to create tests that run fast and predictably.
The idea is to replace the use of e.g. Task.Delay
with an abstraction, the TimeProvider
, that in production
is represented by the TimeProvider.System
, which just uses the real Task.Delay
. During testing it is now possible to
pass in ManualTimeProvider
, which allows the test to control the progress of time, making it possible to skip ahead,
e.g. 10 minutes, and also pause time, leading to fast and predictable tests.
As an example, let us test the "Stuff Service" below that performs specific tasks every 10 seconds with an additional
1-second delay. We have two versions, one that uses the standard types in .NET, and one that uses the TimeProvider
.
// Version of stuff service that uses the built-in DateTimeOffset, PeriodicTimer, and Task.Delay
public class StuffService
{
private static readonly TimeSpan doStuffDelay = TimeSpan.FromSeconds(10);
private readonly List<DateTimeOffset> container;
public StuffService(List<DateTimeOffset> container)
{
this.container = container;
}
public async Task DoStuff(CancellationToken cancelllationToken)
{
using var periodicTimer = new PeriodicTimer(doStuffDelay);
while (await periodicTimer.WaitForNextTickAsync(cancellationToken))
{
await Task.Delay(TimeSpan.FromSeconds(1));
container.Add(DateTimeOffset.UtcNow);
}
}
}
// Version of stuff service that uses the built-in TimeProvider
public class StuffServiceUsingTimeProvider
{
private static readonly TimeSpan doStuffDelay = TimeSpan.FromSeconds(10);
private readonly TimeProvider timeProvider;
private readonly List<DateTimeOffset> container;
public StuffServiceUsingTimeProvider(TimeProvider timeProvider, List<DateTimeOffset> container)
{
this.timeProvider = timeProvider;
this.container = container;
}
public async Task DoStuff(CancellationToken cancelllationToken)
{
using var periodicTimer = timeProvider.CreatePeriodicTimer(doStuffDelay);
while (await periodicTimer.WaitForNextTickAsync(cancellationToken))
{
await timeProvider.Delay(TimeSpan.FromSeconds(1));
container.Add(timeProvider.GetUtcNow());
}
}
}
The test, using xUnit and FluentAssertions, could look like this:
[Fact]
public void DoStuff_does_stuff_every_11_seconds()
{
// Arrange
var timeProvider = new ManualTimeProvider();
var container = new List<DateTimeOffset>();
var sut = new StuffServiceUsingTimeProvider(timeProvider, container);
// Act
_ = sut.DoStuff(CancellationToken.None);
timeProvider.Advance(TimeSpan.FromSeconds(11));
// Assert
container
.Should()
.ContainSingle()
.Which
.Should()
.Be(timeProvider.GetUtcNow());
}
This test will run in nanoseconds and is deterministic.
Compare that to the similar test below for StuffService
that needs to wait for 11 seconds before it can safely assert that the expectation has been met.
[Fact]
public async Task DoStuff_does_stuff_every_11_seconds()
{
// Arrange
var container = new List<DateTimeOffset>();
var sut = new StuffService(container);
// Act
_ = sut.DoStuff(CancellationToken.None);
await Task.Delay(TimeSpan.FromSeconds(11));
// Assert
container
.Should()
.ContainSingle()
.Which
.Should()
.BeCloseTo(DateTimeOffset.UtcNow, precision: TimeSpan.FromMilliseconds(50));
}
Difference between ManualTimeProvider
and FakeTimeProvider
The .NET team has published a similar test-specific time provider, the Microsoft.Extensions.Time.Testing.FakeTimeProvider
.
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:
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()
).
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:
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:
However, FakeTimeProvider
will invoke the timer callback at time 00:03
three times, as illustrated in the drawings below:
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.
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
.
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
.
Jumping to a point in time
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).
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.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Bcl.TimeProvider (>= 8.0.0)
-
net6.0
- Microsoft.Bcl.TimeProvider (>= 8.0.0)
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
1.0.0 | 17,949 | 11/14/2023 |
1.0.0-rc.3 | 2,441 | 11/12/2023 |
1.0.0-rc.2 | 628 | 9/25/2023 |
1.0.0-rc.1 | 104 | 9/19/2023 |
1.0.0-preview.7 | 96 | 9/11/2023 |
1.0.0-preview.6 | 352 | 9/4/2023 |
1.0.0-preview.5 | 522 | 8/21/2023 |
1.0.0-preview.4 | 2,836 | 5/24/2023 |
1.0.0-preview.3 | 87 | 5/24/2023 |
1.0.0-preview.2 | 102 | 5/20/2023 |
1.0.0-preview.1 | 89 | 5/19/2023 |
# Changelog
All notable changes to TimeProviderExtensions will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0]
- Upgrade dependencies to none-preview versions.
## [1.0.0-rc.3]
- Generate strong-named assemblies.
## [1.0.0-rc.2]
- Added `ActiveTimers` property to `ManualTimeProvider`. The property will display the number of currently active timers that have a callback scheduled to be called in the future.
- Allow `ManualTimeProvider.Start` to be set using property initializers.
- Made the timer type created by `ManualTimeProvider`, the `ManualTimer` type, public, and introduced a protected method `CreateManualTimer` on `ManualTimeProvider`. This enables advanced scenarios where a custom `ManualTimer` is needed.
A custom implementation of `ManualTimer` can override the `Change` method and add custom behavior to it.
Overriding `CreateManualTimer` makes it possible to intercept a `TimerCallback` and perform actions before and after the timer callback has been invoked.
- Replace the `AutoAdvanceAmount` property with the `AutoAdvanceBehavior` property on `ManualTimeProvider`, and introduce the `AutoAdvanceBehavior` type. To automatically advance the time when `GetUtcNow()` or `GetLocalNow()` is called, set `AutoAdvanceBehavior.UtcNowAdvanceAmount` to a `TimeSpan` larger than zero.
- Enable auto advance feature for `GetTimestamp()` and `GetElapsedTime(long)`. To automatically advance the time when `GetTimestamp()` or `GetElapsedTime(long)` is called, set `AutoAdvanceBehavior.TimestampAdvanceAmount` to a `TimeSpan` larger than zero.
- `ManualTimer` now exposes its current configuration. `DueTime`, `Period`, `IsActive`, `CallbackTime`, and `CallbackInvokeCount` are now publicly visible.
- Enable auto-advance feature for timers. This enables automatically calling timers callback a specified number of times, by setting the `AutoAdvanceBehavior.TimerAutoTriggerCount` property to a number larger than zero.
## [1.0.0-rc.1]
- Updated Microsoft.Bcl.TimeProvider package dependency to rc.1 version.
## [1.0.0-preview.7]
- Added support for netstandard2.0, as this is supported by the back-port package Microsoft.Bcl.TimeProvider.
## [1.0.0-preview.6]
- Added `Jump(TimeSpan)` and `Jump(DateTimeOffset)` methods that will jump time to the specified place. Any timer callbacks between the start and end of the jump will be invoked the expected number of times, but the date/time returned from `GetUtcNow()` and `GetTimestamp()` will always be the jump time. This differs from how `Advance` and `SetUtcNow` works. See the readme for a detailed description.
## [1.0.0-preview.5]
Aligned the public API surface of `ManualTimeProvider` with `Microsoft.Extensions.Time.Testing.FakeTimeProvider`. This means:
- The `StartTime` property is now called `Start`.
- The `ForwardTime` method has been removed (use `Advance` instead).
- The `AutoAdvanceAmount` property has been introduced, which will advance time with the specified amount every time `GetUtcNow()` is called. It defaults to `TimeSpan.Zero`, which disables auto-advancing.
## [1.0.0-preview.4]
- Added 'StartTime' to `ManualTestProvider`, which represents the initial date/time when the `ManualtTimeProvider` was initialized.
## [1.0.0-preview.3]
- Changed `ManualTestProvider` to set the local time zone to UTC by default, providing a method for overriding during testing.
- Changed the `ManualTestProvider.ToString()` method to return current date time.
- Fixed `ITimer` returned by `ManualTestProvider` such that timers created with a due time equal to zero will fire the timer callback immediately.
## [1.0.0-preview.1]
This release adds a dependency on [Microsoft.Bcl.TimeProvider](Microsoft.Bcl.TimeProvider) and utilizes the types built-in to that to do much of the work.
When using the `ManualTimeProvider` during testing, be aware of these outstanding issues: https://github.com/dotnet/runtime/issues/85326
- Removed `CancelAfter` extension methods. Instead, create a CancellationTokenSource via the method `TimeProvider.CreateCancellationTokenSource(TimeSpan delay)` or in .NET 8, using `new CancellationTokenSource(TimeSpan delay, TimeProvider timeProvider).
**NOTE:** If running on .NET versions earlier than .NET 8.0, there is a constraint when invoking `CancellationTokenSource.CancelAfter(TimeSpan)` on the resultant object. This action will not terminate the initial timer indicated by `delay`. However, this restriction does not apply to .NET 8.0 and later versions.
## [0.8.0]
- Added `TimeProvider.GetElapsedTime(long startingTimestamp)`
- Added `TimeProvider.CreateCancellationTokenSource(TimeSpan delay)`
## [0.7.0]
- Add support for libraries that target netstandard 2.0.
## [0.6.0]
- Changed `TestTimeProvider` to `ManualTimeProvider`.
- `ManualTimeProvider` no longer implements on `IDisposable`.
- Moving time forward using `ManualTimeProvider` will now move time forward in steps, stopping at each scheduled timer/callback time, setting the internal "UtcNow" clock returned from `GetUtcNow()` to invoke the callback, and then progress to the next scheduled timer, until the target "UtcNow" is reached.
## [0.5.0]
- Implemented a shim for the TimeProvider API coming in .NET 8.
- Added support for controlling timestamps during testing.
- Marked the `UtcNow` as obsolete.
## [0.4.0]
- Added support for timers.
## [0.3.0] - 2023-03-03
### Added
- Adds support for canceling a `CancellationTokenSource` after a specific timespan via the `ITimeScheduler.CancelAfter(CancellationTokenSource, TimeSpan)` method.
- Adds a singleton instance property to `DefaultScheduler` that can be used instead of creating a new instance for every use.
### Changed
- All methods in `DefaultScheduler` marked with the `[MethodImpl(MethodImplOptions.AggressiveInlining)]` attribute.
- `TestScheduler.ForwardTime(TimeSpan time)` throws `ArgumentException` if the `time` argument is not positive.
## [0.2.0] - 2023-02-21
Adds support for the `Task.WaitAsync` family of methods.
## [0.1.3-preview] - 2023-01-30
Initial release with support for `Task.Delay` and `PeriodicTimer`.