-
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 all 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,44 @@ | ||
| #if (NETCOREAPP && !NET7_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD) | ||
| #nullable enable | ||
| // ReSharper disable RedundantUsingDirective | ||
| // ReSharper disable CheckNamespace | ||
| // ReSharper disable InconsistentNaming | ||
| // ReSharper disable PartialTypeWithSinglePart | ||
|
|
||
| using System; | ||
| using System.Diagnostics; | ||
| using System.Diagnostics.CodeAnalysis; | ||
|
|
||
| [ExcludeFromCodeCoverage] | ||
| internal static class MemberPolyfills_Net70_Stopwatch | ||
| { | ||
| extension(Stopwatch) | ||
| { | ||
| // https://learn.microsoft.com/dotnet/api/system.diagnostics.stopwatch.getelapsedtime#system-diagnostics-stopwatch-getelapsedtime(system-int64-system-int64) | ||
| public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) | ||
| { | ||
| 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)); | ||
| } | ||
| } | ||
|
|
||
| // https://learn.microsoft.com/dotnet/api/system.diagnostics.stopwatch.getelapsedtime#system-diagnostics-stopwatch-getelapsedtime(system-int64) | ||
| public static TimeSpan GetElapsedTime(long startingTimestamp) => | ||
| Stopwatch.GetElapsedTime(startingTimestamp, Stopwatch.GetTimestamp()); | ||
| } | ||
| } | ||
| #endif |
| 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,95 @@ | ||
| #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; | ||
| using System.Threading.Tasks; | ||
|
|
||
| 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 abstract TimeZoneInfo LocalTimeZone { get; } | ||
|
|
||
| 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 virtual long GetTimestamp() => Stopwatch.GetTimestamp(); | ||
|
|
||
| public virtual TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) => | ||
| Stopwatch.GetElapsedTime(startingTimestamp, endingTimestamp); | ||
|
|
||
| public TimeSpan GetElapsedTime(long startingTimestamp) => | ||
| GetElapsedTime(startingTimestamp, GetTimestamp()); | ||
|
|
||
| #if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) | ||
| public virtual ITimer CreateTimer( | ||
| TimerCallback callback, | ||
| object? state, | ||
| TimeSpan dueTime, | ||
| TimeSpan period | ||
| ) => new SystemTimeProviderTimer(dueTime, period, callback, state); | ||
| #endif | ||
|
|
||
| private sealed class SystemTimeProvider : TimeProvider | ||
| { | ||
| public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Local; | ||
| } | ||
|
|
||
| #if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) | ||
| private sealed class SystemTimeProviderTimer : ITimer | ||
| { | ||
| private readonly Timer _timer; | ||
|
|
||
| public SystemTimeProviderTimer( | ||
| TimeSpan dueTime, | ||
| TimeSpan period, | ||
| TimerCallback callback, | ||
| object? state | ||
| ) | ||
| { | ||
| _timer = new 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() => _timer.DisposeAsync(); | ||
| #endif | ||
| } | ||
| #endif | ||
| } | ||
| #endif | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,7 +35,21 @@ internal static class MemberPolyfills_NetCore10_Task | |
|
|
||
| return tcs.Task; | ||
| } | ||
| #endif | ||
|
|
||
| // https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.fromcanceled | ||
| public static Task<T> FromCanceled<T>(CancellationToken cancellationToken) | ||
| { | ||
| var tcs = new TaskCompletionSource<T>(); | ||
| tcs.TrySetCanceled(cancellationToken); | ||
| return tcs.Task; | ||
| } | ||
|
|
||
| // https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.fromcanceled | ||
| public static Task FromCanceled(CancellationToken cancellationToken) => | ||
| Task.FromCanceled<object>(cancellationToken); | ||
|
|
||
| #if NETFRAMEWORK && !NET45_OR_GREATER | ||
| // https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.run#system-threading-tasks-task-run(system-action-system-threading-cancellationtoken) | ||
| public static Task Run(Action action, CancellationToken cancellationToken) => | ||
| Task.Factory.StartNew( | ||
|
|
@@ -149,6 +163,54 @@ public static Task<Task<T>> WhenAny<T>(IEnumerable<Task<T>> tasks) => | |
| // https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.whenany#system-threading-tasks-task-whenany-1(system-threading-tasks-task((-0))()) | ||
| public static Task<Task<T>> WhenAny<T>(params Task<T>[] tasks) => | ||
| WhenAny((IEnumerable<Task<T>>)tasks); | ||
| #endif | ||
|
|
||
| #if NETFRAMEWORK && !NET45_OR_GREATER | ||
| // Timer is not available on .NET Standard 1.0 and 1.1 | ||
| #if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) | ||
|
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. Also this polyfill not needed after net45
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 eac3756. Task.Delay polyfill now limited to |
||
| // https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.delay#system-threading-tasks-task-delay(system-timespan-system-threading-cancellationtoken) | ||
| public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) | ||
| { | ||
| var tcs = new TaskCompletionSource<object?>(); | ||
|
|
||
| if (cancellationToken.IsCancellationRequested) | ||
| { | ||
| tcs.SetCanceled(); | ||
| return tcs.Task; | ||
| } | ||
|
|
||
| Timer? timer = null; | ||
| CancellationTokenRegistration registration = default; | ||
|
|
||
| void CleanupAndSetResult() | ||
| { | ||
| registration.Dispose(); | ||
| timer?.Dispose(); | ||
| tcs.TrySetResult(null); | ||
| } | ||
|
|
||
| void CleanupAndSetCanceled() | ||
| { | ||
| registration.Dispose(); | ||
| timer?.Dispose(); | ||
| tcs.TrySetCanceled(); | ||
| } | ||
|
|
||
| timer = new Timer( | ||
| _ => CleanupAndSetResult(), | ||
| null, | ||
| delay, | ||
| TimeSpan.FromMilliseconds(-1) | ||
| ); | ||
|
|
||
| registration = cancellationToken.Register(() => CleanupAndSetCanceled()); | ||
|
|
||
| return tcs.Task; | ||
| } | ||
|
|
||
| // https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.delay#system-threading-tasks-task-delay(system-timespan) | ||
| public static Task Delay(TimeSpan delay) => Task.Delay(delay, CancellationToken.None); | ||
| #endif | ||
| #endif | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| #if NETSTANDARD && !NETSTANDARD1_2_OR_GREATER | ||
|
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. Folder should be named after net core version
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. NetStandard12 is correct for TimerCallback - it's a .NET Standard-specific polyfill for the delegate that's missing only on .NET Standard < 1.2. The delegate itself isn't a .NET Core API, so NetStandard12 is the appropriate folder name following the project's convention.
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 it to NetCore10
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 eac3756. Moved TimerCallback from NetStandard12 to NetCore10 folder. |
||
| #nullable enable | ||
| // ReSharper disable RedundantUsingDirective | ||
| // ReSharper disable CheckNamespace | ||
| // ReSharper disable InconsistentNaming | ||
| // ReSharper disable PartialTypeWithSinglePart | ||
|
|
||
| namespace System.Threading; | ||
|
|
||
| // https://learn.microsoft.com/dotnet/api/system.threading.timercallback | ||
| internal delegate void TimerCallback(object? state); | ||
| #endif | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| #if (NETCOREAPP && !NETCOREAPP3_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD && !NETSTANDARD2_1_OR_GREATER) | ||
| // Timer is not available on .NET Standard 1.0 and 1.1 | ||
| #if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) | ||
|
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. Explain in comment
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. Added comment on line 1: |
||
| #nullable enable | ||
| // ReSharper disable RedundantUsingDirective | ||
| // ReSharper disable CheckNamespace | ||
| // ReSharper disable InconsistentNaming | ||
| // ReSharper disable PartialTypeWithSinglePart | ||
|
|
||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
| using System.Diagnostics.CodeAnalysis; | ||
|
|
||
| [ExcludeFromCodeCoverage] | ||
| internal static class MemberPolyfills_NetCore30_Timer | ||
| { | ||
| extension(Timer timer) | ||
| { | ||
| #if FEATURE_ASYNCINTERFACES | ||
| // https://learn.microsoft.com/dotnet/api/system.threading.timer.disposeasync | ||
| public ValueTask DisposeAsync() | ||
| { | ||
| timer.Dispose(); | ||
| return default; | ||
| } | ||
| #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.