Add polyfills for TimeProvider and ITimer#79
Conversation
Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
…kage Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
PolyShim/Net80/ITimer.cs
Outdated
| @@ -0,0 +1,26 @@ | |||
| #if !FEATURE_TIMEPROVIDER && ((NETCOREAPP && !NET8_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD)) | |||
There was a problem hiding this comment.
The conditional symbol already includes TFMs as well
There was a problem hiding this comment.
Fixed in a4b3590. Simplified to just #if !FEATURE_TIMEPROVIDER since the feature flag already includes the TFM checks.
PolyShim/Net80/ITimer.cs
Outdated
| @@ -0,0 +1,26 @@ | |||
| #if !FEATURE_TIMEPROVIDER && ((NETCOREAPP && !NET8_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD)) | |||
| // Only include ITimer if we have Task support and are on .NET Standard 2.0+, .NET Core 2.0+, or .NET Framework 4.6.1+ | |||
| #if FEATURE_TASK && (NETSTANDARD2_0_OR_GREATER || NETCOREAPP2_0_OR_GREATER || NET461_OR_GREATER) | |||
There was a problem hiding this comment.
Nothing below appears to require Tasks or those TFMs?
There was a problem hiding this comment.
Fixed in a4b3590. Removed the unnecessary FEATURE_TASK and TFM conditionals from ITimer - it doesn't actually require Task types.
| namespace System.Threading; | ||
|
|
||
| // https://learn.microsoft.com/dotnet/api/system.threading.itimer | ||
| internal interface ITimer : IDisposable |
There was a problem hiding this comment.
Removed ExcludeFromCodeCoverage in a4b3590. The attribute isn't supported on interfaces in older C# versions.
PolyShim/Net80/TimeProvider.cs
Outdated
| [ExcludeFromCodeCoverage] | ||
| internal abstract class TimeProvider | ||
| { | ||
| private static readonly TimeProvider s_system = new SystemTimeProvider(); |
There was a problem hiding this comment.
Inlined in a4b3590. Now using SystemThreading.Timeout.InfiniteTimeSpan directly instead of storing in a constant.
PolyShim/Net80/TimeProvider.cs
Outdated
| TimeSpan period | ||
| ) | ||
| { | ||
| if (callback is null) |
There was a problem hiding this comment.
Don't bother with guards in polyfills
There was a problem hiding this comment.
Removed null guard in a4b3590. Changed CreateTimer to single-expression method body.
PolyShim/Net80/TimeProvider.cs
Outdated
| #if NETSTANDARD2_0 || NET461 || NET462 | ||
| return Task.FromResult(false); | ||
| #else | ||
| return Task.CompletedTask; |
There was a problem hiding this comment.
We have this polyfilled for all TFMs
There was a problem hiding this comment.
Fixed in a4b3590. Now using the polyfilled Task.CompletedTask directly without conditional compilation.
PolyShim/Net80/TimeProvider.cs
Outdated
| if (cancellationToken.IsCancellationRequested) | ||
| { | ||
| #if NETCOREAPP || NETSTANDARD2_1_OR_GREATER | ||
| return Task.FromCanceled(cancellationToken); |
There was a problem hiding this comment.
Pretty sure we have this polyfilled too
There was a problem hiding this comment.
Task.FromCanceled is not actually polyfilled in the codebase. Used TaskCompletionSource.SetCanceled() as an alternative in a4b3590.
PolyShim/Net80/TimeProvider.cs
Outdated
| #if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER | ||
| return _timer.DisposeAsync(); | ||
| #else | ||
| _timer.Dispose(); | ||
| return default; |
There was a problem hiding this comment.
Just try cast to IAsyncDisposable
There was a problem hiding this comment.
The try-cast approach doesn't work because Timer doesn't implement IAsyncDisposable on net461 and similar frameworks - the compiler rejects the cast at compile time. Kept the conditional compilation in a4b3590 to match the actual Timer API availability.
PolyShim/Net80/TimeProvider.cs
Outdated
| // Only include TimeProvider if we have Task support and are on .NET Standard 2.0+, .NET Core 2.0+, or .NET Framework 4.6.1+ | ||
| // This matches the support level of Microsoft.Bcl.TimeProvider compatibility package | ||
| #if FEATURE_TASK && (NETSTANDARD2_0_OR_GREATER || NETCOREAPP2_0_OR_GREATER || NET461_OR_GREATER) | ||
| #nullable enable |
There was a problem hiding this comment.
Pretty sure we can support all TFMs with it. APIs that need Task can be singled out.
There was a problem hiding this comment.
Fixed in a4b3590. Removed TFM-specific guards and made all TFMs supported. The Task-dependent methods (Delay, CreateCancellationTokenSource) are now conditional on FEATURE_TASK with appropriate fallback error messages for older frameworks that lack the required APIs.
…se polyfilled APIs Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
PolyShim/Net80/TimeProvider.cs
Outdated
| using System.Diagnostics; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using SystemThreading = System.Threading; | ||
| #if FEATURE_TASK |
There was a problem hiding this comment.
I think this can be referenced unconditionally because we have a dummy in that namespace
There was a problem hiding this comment.
Fixed in 65710d0. Removed the conditional since there's a namespace dummy for System.Threading.Tasks.
PolyShim/Net80/TimeProvider.cs
Outdated
| [ExcludeFromCodeCoverage] | ||
| internal abstract class TimeProvider | ||
| { | ||
| private static readonly TimeProvider s_system = new SystemTimeProvider(); |
There was a problem hiding this comment.
Inlined in 65710d0. Now using auto-property with initializer: public static TimeProvider System { get; } = new SystemTimeProvider();
PolyShim/Net80/TimeProvider.cs
Outdated
| } | ||
| } | ||
|
|
||
| #if !NETSTANDARD1_0 && !NETSTANDARD1_1 |
There was a problem hiding this comment.
Why? Explain with a comment. Also, use this pattern (pseudo): if net standard and not best standard 1.2 or greater
There was a problem hiding this comment.
Fixed in 65710d0. Added comment explaining Timer/TimerCallback aren't available on .NET Standard 1.0/1.1, and changed pattern to !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER).
PolyShim/Net80/TimeProvider.cs
Outdated
| SystemThreading.CancellationToken cancellationToken = default | ||
| ) | ||
| { | ||
| #if NETSTANDARD1_3_OR_GREATER || NETCOREAPP || NET45_OR_GREATER |
There was a problem hiding this comment.
Fixed in 65710d0. The nested conditional was unnecessary - moved the TFM check to the method-level conditional. Delay and CreateCancellationTokenSource now only exist on frameworks that have the required APIs (.NET 4.5+, .NET Standard 1.3+, .NET Core 1.0+).
PolyShim/Net80/TimeProvider.cs
Outdated
|
|
||
| public SystemThreading.CancellationTokenSource CreateCancellationTokenSource(TimeSpan delay) | ||
| { | ||
| #if NETSTANDARD1_3_OR_GREATER || NETCOREAPP || NET45_OR_GREATER |
There was a problem hiding this comment.
Fixed in 65710d0. Same as above - moved the TFM check to method-level conditional instead of having nested conditionals inside the method body.
…ystem property, add comments, fix conditionals Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
PolyShim/Net80/TimeProvider.cs
Outdated
| return new DateTimeOffset(utcDateTime.DateTime + offset, offset); | ||
| } | ||
|
|
||
| public abstract TimeZoneInfo LocalTimeZone { get; } |
There was a problem hiding this comment.
Properties should be above methods
There was a problem hiding this comment.
Fixed in be2f69e. Moved LocalTimeZone property above GetUtcNow() method.
PolyShim/Net80/TimeProvider.cs
Outdated
|
|
||
| public virtual TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) | ||
| { | ||
| // Stopwatch.GetElapsedTime was added in .NET 7, so we need to calculate it manually |
There was a problem hiding this comment.
Added in be2f69e. Created Net70/Stopwatch.cs polyfill with GetElapsedTime(long startingTimestamp, long endingTimestamp) and GetElapsedTime(long startingTimestamp) extension methods. TimeProvider now delegates to the polyfilled method.
PolyShim/Net80/TimeProvider.cs
Outdated
| } | ||
| } | ||
|
|
||
| // Timer and TimerCallback are not available on .NET Standard 1.0 and 1.1 |
There was a problem hiding this comment.
ITimer is polyfilled. Can we polyfill TimerCallback?
There was a problem hiding this comment.
Added in be2f69e. Created NetStandard12/TimerCallback.cs polyfill defining the delegate for .NET Standard < 1.2. CreateTimer is now available on all frameworks except .NET Standard 1.0/1.1.
PolyShim/Net80/TimeProvider.cs
Outdated
| ) => new SystemTimeProviderTimer(dueTime, period, callback, state); | ||
| #endif | ||
|
|
||
| // Task.Delay, Timeout.InfiniteTimeSpan, and CancellationTokenSource(TimeSpan) require .NET 4.5+ |
There was a problem hiding this comment.
Polyfill the missing members so they're available everywhere where Task is available (except infinite timeout, that should be available everywhere).
There was a problem hiding this comment.
Added in be2f69e. Created Net45/Task.cs with Task.Delay polyfill for frameworks < .NET 4.5. Removed all nested conditionals - Delay and CreateCancellationTokenSource are now available wherever FEATURE_TASK is defined. Note: CancellationTokenSource(TimeSpan) constructor doesn't exist on .NET 4.0, so CreateCancellationTokenSource returns a plain instance for those frameworks.
PolyShim/Net80/TimeProvider.cs
Outdated
| return Task.Delay(delay, cancellationToken); | ||
| } | ||
|
|
||
| public SystemThreading.CancellationTokenSource CreateCancellationTokenSource(TimeSpan delay) |
There was a problem hiding this comment.
Do we need a fully qualified return type?
There was a problem hiding this comment.
Fixed in be2f69e. Changed from SystemThreading.CancellationTokenSource to just CancellationTokenSource by updating using statements.
PolyShim/Net80/TimeProvider.cs
Outdated
|
|
||
| public SystemThreading.CancellationTokenSource CreateCancellationTokenSource(TimeSpan delay) | ||
| { | ||
| if (delay < TimeSpan.Zero && delay != SystemThreading.Timeout.InfiniteTimeSpan) |
There was a problem hiding this comment.
Fixed in be2f69e. Removed all ArgumentOutOfRangeException validation checks from Delay() and CreateCancellationTokenSource().
PolyShim/Net80/TimeProvider.cs
Outdated
| #if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) | ||
| private sealed class SystemTimeProviderTimer : SystemThreading.ITimer | ||
| { | ||
| private readonly SystemThreading.Timer _timer; |
There was a problem hiding this comment.
Timer itself is part of the BCL and can't be polyfilled as it's a native type. However, the CreateTimer method now works on all frameworks where Timer is available (everywhere except .NET Standard 1.0/1.1) thanks to the TimerCallback polyfill.
PolyShim/Net80/TimeProvider.cs
Outdated
| public ValueTask DisposeAsync() | ||
| { | ||
| #if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER | ||
| return _timer.DisposeAsync(); |
There was a problem hiding this comment.
Just polyfill this method on Timer, same as we do for Stream
There was a problem hiding this comment.
Added in be2f69e. Created NetCore30/Timer.cs with DisposeAsync() extension method. SystemTimeProviderTimer now calls _timer.DisposeAsync() directly, which uses the polyfill on older frameworks.
…simplify code Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
PolyShim/Net45/Task.cs
Outdated
| // Timer is required for Task.Delay implementation | ||
| #if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) |
There was a problem hiding this comment.
Can we use Thread Sleep there? If not, move this guard to cover the actual members only
There was a problem hiding this comment.
Fixed in a322476. Moved the Timer availability guard (!(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER)) to wrap only the Task.Delay members, keeping the outer FEATURE_TASK guard at the file level.
PolyShim/Net45/Task.cs
Outdated
| TimeSpan.FromMilliseconds(-1) | ||
| ); | ||
|
|
||
| cancellationToken.Register(() => |
There was a problem hiding this comment.
Fixed in a322476. Changed to use nullable Timer pattern - timer disposes itself in both the callback and the cancellation handler, preventing resource leaks.
There was a problem hiding this comment.
I mean that the cancellation registration needs to be disposed
There was a problem hiding this comment.
Fixed in f7dfa0d. Now capturing the registration result and disposing it when the task completes: var registration = cancellationToken.Register(...) followed by tcs.Task.ContinueWith(_ => registration.Dispose(), ...)
PolyShim/Net80/TimeProvider.cs
Outdated
| #if NET45_OR_GREATER || NETSTANDARD1_3_OR_GREATER || NETCOREAPP | ||
| return new CancellationTokenSource(delay); | ||
| #else | ||
| // CancellationTokenSource(int) constructor added in .NET 4.5 | ||
| // For older frameworks, just return a plain instance | ||
| return new CancellationTokenSource(); | ||
| #endif |
There was a problem hiding this comment.
Or use parameterless constructor and call CancelAfter
There was a problem hiding this comment.
Fixed in a322476. Changed to use parameterless constructor and call CancelAfter() method instead of using the constructor overload.
PolyShim/NetCore30/Timer.cs
Outdated
| @@ -0,0 +1,25 @@ | |||
| #if FEATURE_ASYNCINTERFACES && !(NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER) | |||
There was a problem hiding this comment.
Move the feature guard to the member. Rewrite the TFM guard to be consistent with others.
There was a problem hiding this comment.
Fixed in a322476. Moved FEATURE_ASYNCINTERFACES guard to the DisposeAsync member. Changed TFM guard from FEATURE_ASYNCINTERFACES && !(NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER) to (NETCOREAPP && !NETCOREAPP3_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD && !NETSTANDARD2_1_OR_GREATER) with additional nested guard for Timer availability.
…embers Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
|
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
…TimeProvider API Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com>
|
Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
There was a problem hiding this comment.
Pull request overview
This PR adds polyfills for TimeProvider and ITimer introduced in .NET 8, enabling time abstraction APIs on older frameworks. The implementation includes the core TimeProvider abstract class with its System singleton, along with supporting polyfills for Stopwatch.GetElapsedTime, TimerCallback delegate, Task.Delay, Task.FromCanceled, and Timer.DisposeAsync to ensure broad framework compatibility.
Changes:
- Added TimeProvider and ITimer polyfills for .NET 8 features with automatic feature detection for Microsoft.Bcl.TimeProvider package
- Added supporting polyfills (Stopwatch.GetElapsedTime, TimerCallback, Task.Delay, Task.FromCanceled, Timer.DisposeAsync) to enable TimeProvider on older frameworks
- Updated package references and documentation to include Microsoft.Bcl.TimeProvider compatibility package
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| Readme.md | Added Microsoft.Bcl.TimeProvider to the list of recognized compatibility packages |
| PolyShim/Signatures.md | Updated signature counts and added documentation for new polyfilled APIs (TimeProvider, ITimer, Stopwatch methods, Task methods, Timer.DisposeAsync) |
| PolyShim/PolyShim.targets | Added FEATURE_TIMEPROVIDER detection for .NET 8+ and Microsoft.Bcl.TimeProvider package |
| PolyShim/PolyShim.csproj | Added Microsoft.Bcl.TimeProvider package references for various target frameworks; upgraded Microsoft.Bcl.AsyncInterfaces to 8.0.0 |
| PolyShim/NetCore30/Timer.cs | Added DisposeAsync extension method for Timer on older frameworks |
| PolyShim/NetCore10/TimerCallback.cs | Added TimerCallback delegate polyfill for .NET Standard < 1.2 |
| PolyShim/NetCore10/Task.cs | Added Task.Delay and Task.FromCanceled polyfills for .NET Framework < 4.5 |
| PolyShim/Net80/TimeProvider.cs | Core TimeProvider abstract class implementation with System singleton and timer creation |
| PolyShim/Net80/ITimer.cs | ITimer interface with conditional IAsyncDisposable support |
| PolyShim/Net70/Stopwatch.cs | Added GetElapsedTime static methods for calculating elapsed time from timestamps |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <PackageReference | ||
| Include="Microsoft.Bcl.AsyncInterfaces" | ||
| Version="7.0.0" | ||
| Version="8.0.0" | ||
| PrivateAssets="all" | ||
| Condition="'$(TargetFrameworkIdentifier)' == '.NETStandard' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '2.0'))" | ||
| /> | ||
| <PackageReference | ||
| Include="Microsoft.Bcl.AsyncInterfaces" | ||
| Version="7.0.0" | ||
| Version="8.0.0" | ||
| PrivateAssets="all" | ||
| Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '2.0')) AND $([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '3.0'))" | ||
| /> | ||
| <PackageReference | ||
| Include="Microsoft.Bcl.AsyncInterfaces" | ||
| Version="7.0.0" | ||
| Version="8.0.0" | ||
| PrivateAssets="all" | ||
| Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '4.6.1'))" | ||
| /> |
There was a problem hiding this comment.
The Microsoft.Bcl.AsyncInterfaces package has been upgraded from version 7.0.0 to 8.0.0 in this PR, but this change is not mentioned in the PR description. While this upgrade appears intentional and consistent with other package updates, it should be documented as it could affect consumers of this library.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
TimeProvider and ITimer
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #79 +/- ##
============================
============================
☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Adds polyfills for
TimeProviderandITimerintroduced in .NET 8, enabling time abstraction APIs on older frameworks.Core Polyfills
TimeProvider (
Net80/TimeProvider.cs) - Abstract class withSystemsingleton property and methods for time operationsGetUtcNow()- Returns current UTC time as DateTimeOffsetGetLocalNow()- Returns current local timeGetTimestamp()- Returns high-resolution timestamp for performance measurementsGetElapsedTime()- Two overloads for calculating elapsed time from timestampsCreateTimer()- Creates timer instances (virtual method for testability)LocalTimeZoneproperty - Returns the local time zoneITimer (
Net80/ITimer.cs) - Interface for timer abstraction withIAsyncDisposablesupportSupporting Polyfills Created
To enable TimeProvider on the broadest range of frameworks, the following polyfills were added:
Net70/Stopwatch.cs) - Extension methods for calculating elapsed time from timestampsNetCore10/TimerCallback.cs) - Delegate for timer callbacks on .NET Standard < 1.2NetCore10/Task.cs) - Asynchronous delay implementation for .NET Framework < 4.5NetCore10/Task.cs) - Creates canceled tasks (generic and non-generic overloads)NetCore30/Timer.cs) - Extension method for async timer disposal on older frameworksImplementation Details
Feature Detection
Microsoft.Bcl.TimeProvidercompatibility packageThe polyfills are automatically disabled on .NET 8+ or when
Microsoft.Bcl.TimeProvideris referenced.Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.