-
Notifications
You must be signed in to change notification settings - Fork 5
Add polyfills for TimeProvider and ITimer
#79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
264ad04
4149ee0
01cac72
a4b3590
65710d0
be2f69e
a322476
f7dfa0d
c1b7ce5
eac3756
2d535b0
50d462d
46832c5
48fe816
11cb66f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| #if !FEATURE_TIMEPROVIDER | ||
| #nullable enable | ||
| // ReSharper disable RedundantUsingDirective | ||
| // ReSharper disable CheckNamespace | ||
| // ReSharper disable InconsistentNaming | ||
| // ReSharper disable PartialTypeWithSinglePart | ||
|
|
||
| using System; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace System.Threading; | ||
|
|
||
| // https://learn.microsoft.com/dotnet/api/system.threading.itimer | ||
| internal interface ITimer : IDisposable | ||
| #if FEATURE_ASYNCINTERFACES | ||
| , IAsyncDisposable | ||
| #endif | ||
| { | ||
| bool Change(TimeSpan dueTime, TimeSpan period); | ||
| } | ||
| #endif | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| #if !FEATURE_TIMEPROVIDER | ||
| #nullable enable | ||
| // ReSharper disable RedundantUsingDirective | ||
| // ReSharper disable CheckNamespace | ||
| // ReSharper disable InconsistentNaming | ||
| // ReSharper disable PartialTypeWithSinglePart | ||
|
|
||
| using System; | ||
| using System.Diagnostics; | ||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Threading.Tasks; | ||
| using SystemThreading = System.Threading; | ||
|
|
||
| namespace System; | ||
|
|
||
| // https://learn.microsoft.com/dotnet/api/system.timeprovider | ||
| [ExcludeFromCodeCoverage] | ||
| internal abstract class TimeProvider | ||
| { | ||
| public static TimeProvider System { get; } = new SystemTimeProvider(); | ||
|
|
||
| protected TimeProvider() { } | ||
|
|
||
| public virtual DateTimeOffset GetUtcNow() => DateTimeOffset.UtcNow; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this depend on GetTimestamp?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, GetUtcNow() should not depend on GetTimestamp(). They serve different purposes - GetUtcNow() provides the current time as DateTimeOffset, while GetTimestamp() returns high-resolution timestamps for performance measurements. The default implementation correctly returns DateTimeOffset.UtcNow. |
||
|
|
||
| public DateTimeOffset GetLocalNow() | ||
| { | ||
| var utcDateTime = GetUtcNow(); | ||
| var offset = LocalTimeZone.GetUtcOffset(utcDateTime); | ||
| return new DateTimeOffset(utcDateTime.DateTime + offset, offset); | ||
| } | ||
|
|
||
| public abstract TimeZoneInfo LocalTimeZone { get; } | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Properties should be above methods
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in be2f69e. Moved LocalTimeZone property above GetUtcNow() method. |
||
|
|
||
| public virtual long GetTimestamp() => Stopwatch.GetTimestamp(); | ||
|
|
||
| public TimeSpan GetElapsedTime(long startingTimestamp) => | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Move below the other overload
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in f7dfa0d. Moved |
||
| GetElapsedTime(startingTimestamp, GetTimestamp()); | ||
|
|
||
| public virtual TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) | ||
| { | ||
| // Stopwatch.GetElapsedTime was added in .NET 7, so we need to calculate it manually | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or we can polyfill that?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in be2f69e. Created |
||
| var tickFrequency = Stopwatch.Frequency; | ||
| var ticks = endingTimestamp - startingTimestamp; | ||
|
|
||
| if (tickFrequency == TimeSpan.TicksPerSecond) | ||
| { | ||
| return new TimeSpan(ticks); | ||
| } | ||
| else if (tickFrequency > TimeSpan.TicksPerSecond) | ||
| { | ||
| var ticksPerStopwatchTick = (double)tickFrequency / TimeSpan.TicksPerSecond; | ||
| return new TimeSpan((long)(ticks / ticksPerStopwatchTick)); | ||
| } | ||
| else | ||
| { | ||
| var ticksPerStopwatchTick = (double)TimeSpan.TicksPerSecond / tickFrequency; | ||
| return new TimeSpan((long)(ticks * ticksPerStopwatchTick)); | ||
| } | ||
| } | ||
|
|
||
| // Timer and TimerCallback are not available on .NET Standard 1.0 and 1.1 | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ITimer is polyfilled. Can we polyfill TimerCallback?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in be2f69e. Created |
||
| #if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) | ||
| public SystemThreading.ITimer CreateTimer( | ||
| SystemThreading.TimerCallback callback, | ||
| object? state, | ||
| TimeSpan dueTime, | ||
| TimeSpan period | ||
| ) => new SystemTimeProviderTimer(dueTime, period, callback, state); | ||
| #endif | ||
|
|
||
| // Task.Delay, Timeout.InfiniteTimeSpan, and CancellationTokenSource(TimeSpan) require .NET 4.5+ | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Polyfill the missing members so they're available everywhere where Task is available (except infinite timeout, that should be available everywhere).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in be2f69e. Created |
||
| #if FEATURE_TASK && (NETSTANDARD1_3_OR_GREATER || NETCOREAPP || NET45_OR_GREATER) | ||
| public virtual Task Delay( | ||
| TimeSpan delay, | ||
| SystemThreading.CancellationToken cancellationToken = default | ||
| ) | ||
| { | ||
| if (delay < TimeSpan.Zero && delay != SystemThreading.Timeout.InfiniteTimeSpan) | ||
| throw new ArgumentOutOfRangeException(nameof(delay)); | ||
|
|
||
| if (cancellationToken.IsCancellationRequested) | ||
| { | ||
| var tcs = new TaskCompletionSource<bool>(); | ||
| tcs.SetCanceled(); | ||
| return tcs.Task; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use Task.FromCancellation or whatever. If unavailable, add Polyfill
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in f7dfa0d. Created |
||
| } | ||
|
|
||
| if (delay == TimeSpan.Zero) | ||
| return Task.CompletedTask; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have this polyfilled for all TFMs
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in a4b3590. Now using the polyfilled Task.CompletedTask directly without conditional compilation. |
||
|
|
||
| return Task.Delay(delay, cancellationToken); | ||
| } | ||
|
|
||
| public SystemThreading.CancellationTokenSource CreateCancellationTokenSource(TimeSpan delay) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need a fully qualified return type?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in be2f69e. Changed from |
||
| { | ||
| if (delay < TimeSpan.Zero && delay != SystemThreading.Timeout.InfiniteTimeSpan) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Drop the guards in all polyfills
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in be2f69e. Removed all |
||
| throw new ArgumentOutOfRangeException(nameof(delay)); | ||
|
|
||
| if (delay == SystemThreading.Timeout.InfiniteTimeSpan) | ||
| return new SystemThreading.CancellationTokenSource(); | ||
|
|
||
| return new SystemThreading.CancellationTokenSource(delay); | ||
| } | ||
| #endif | ||
|
|
||
| private sealed class SystemTimeProvider : TimeProvider | ||
| { | ||
| public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Local; | ||
| } | ||
|
|
||
| // Timer and TimerCallback are not available on .NET Standard 1.0 and 1.1 | ||
| #if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) | ||
| private sealed class SystemTimeProviderTimer : SystemThreading.ITimer | ||
| { | ||
| private readonly SystemThreading.Timer _timer; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this be polyfilled?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
|
|
||
| public SystemTimeProviderTimer( | ||
| TimeSpan dueTime, | ||
| TimeSpan period, | ||
| SystemThreading.TimerCallback callback, | ||
| object? state | ||
| ) | ||
| { | ||
| _timer = new SystemThreading.Timer(callback, state, dueTime, period); | ||
| } | ||
|
|
||
| public bool Change(TimeSpan dueTime, TimeSpan period) | ||
| { | ||
| try | ||
| { | ||
| return _timer.Change(dueTime, period); | ||
| } | ||
| catch | ||
| { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| _timer.Dispose(); | ||
| } | ||
|
|
||
| #if FEATURE_ASYNCINTERFACES | ||
| public ValueTask DisposeAsync() | ||
| { | ||
| #if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER | ||
| return _timer.DisposeAsync(); | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just polyfill this method on Timer, same as we do for Stream
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added in be2f69e. Created |
||
| #else | ||
| _timer.Dispose(); | ||
| return default; | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just try cast to IAsyncDisposable
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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. |
||
| #endif | ||
| } | ||
| #endif | ||
| } | ||
| #endif | ||
| } | ||
| #endif | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -50,19 +50,19 @@ | |
| <ItemGroup> | ||
| <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'))" | ||
| /> | ||
|
Comment on lines
51
to
68
|
||
|
|
@@ -144,6 +144,24 @@ | |
| PrivateAssets="all" | ||
| Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework' AND $([MSBuild]::VersionEquals($(TargetFrameworkVersion), '4.0'))" | ||
| /> | ||
| <PackageReference | ||
| Include="Microsoft.Bcl.TimeProvider" | ||
| Version="8.0.1" | ||
| PrivateAssets="all" | ||
| Condition="'$(TargetFrameworkIdentifier)' == '.NETStandard' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '2.0'))" | ||
| /> | ||
| <PackageReference | ||
| Include="Microsoft.Bcl.TimeProvider" | ||
| Version="8.0.1" | ||
| PrivateAssets="all" | ||
| Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '2.0')) AND $([MSBuild]::VersionLessThan($(TargetFrameworkVersion), '8.0'))" | ||
| /> | ||
| <PackageReference | ||
| Include="Microsoft.Bcl.TimeProvider" | ||
| Version="8.0.1" | ||
| PrivateAssets="all" | ||
| Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework' AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '4.6.2'))" | ||
| /> | ||
| </ItemGroup> | ||
|
|
||
| <!-- Import common .NET Framework libraries for metadata references --> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exclude from coverage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed ExcludeFromCodeCoverage in a4b3590. The attribute isn't supported on interfaces in older C# versions.