From 264ad0459ef4285adaa91f8213d0fbac0dcad43c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:34:04 +0000 Subject: [PATCH 01/15] Initial plan From 4149ee0cb3299b0a48953768ec52aec8bcc34b6a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:44:35 +0000 Subject: [PATCH 02/15] Add TimeProvider and ITimer polyfills for .NET 8 Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- PolyShim/Net80/ITimer.cs | 26 +++++ PolyShim/Net80/TimeProvider.cs | 181 +++++++++++++++++++++++++++++++++ PolyShim/PolyShim.csproj | 24 ++++- PolyShim/PolyShim.targets | 17 ++++ PolyShim/Signatures.md | 8 +- 5 files changed, 251 insertions(+), 5 deletions(-) create mode 100644 PolyShim/Net80/ITimer.cs create mode 100644 PolyShim/Net80/TimeProvider.cs diff --git a/PolyShim/Net80/ITimer.cs b/PolyShim/Net80/ITimer.cs new file mode 100644 index 00000000..b3967850 --- /dev/null +++ b/PolyShim/Net80/ITimer.cs @@ -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) +#nullable enable +// ReSharper disable RedundantUsingDirective +// ReSharper disable CheckNamespace +// ReSharper disable InconsistentNaming +// ReSharper disable PartialTypeWithSinglePart + +using System; +using System.Diagnostics.CodeAnalysis; +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 +#endif diff --git a/PolyShim/Net80/TimeProvider.cs b/PolyShim/Net80/TimeProvider.cs new file mode 100644 index 00000000..3ff185c2 --- /dev/null +++ b/PolyShim/Net80/TimeProvider.cs @@ -0,0 +1,181 @@ +#if !FEATURE_TIMEPROVIDER && ((NETCOREAPP && !NET8_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD)) +// 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 +// 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 +{ + private static readonly TimeProvider s_system = new SystemTimeProvider(); + private static readonly TimeSpan s_infiniteTimeSpan = SystemThreading.Timeout.InfiniteTimeSpan; + + public static TimeProvider System => s_system; + + protected TimeProvider() { } + + public virtual DateTimeOffset GetUtcNow() => 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; } + + public virtual long GetTimestamp() => Stopwatch.GetTimestamp(); + + public TimeSpan GetElapsedTime(long startingTimestamp) => + 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 + 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)); + } + } + + public SystemThreading.ITimer CreateTimer( + SystemThreading.TimerCallback callback, + object? state, + TimeSpan dueTime, + TimeSpan period + ) + { + if (callback is null) + throw new ArgumentNullException(nameof(callback)); + + return new SystemTimeProviderTimer(dueTime, period, callback, state); + } + + public virtual Task Delay( + TimeSpan delay, + SystemThreading.CancellationToken cancellationToken = default + ) + { + if (delay < TimeSpan.Zero && delay != s_infiniteTimeSpan) + throw new ArgumentOutOfRangeException(nameof(delay)); + + if (cancellationToken.IsCancellationRequested) + { +#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER + return Task.FromCanceled(cancellationToken); +#else + var tcs = new TaskCompletionSource(); + tcs.SetCanceled(); + return tcs.Task; +#endif + } + + if (delay == TimeSpan.Zero) + { +#if NETSTANDARD2_0 || NET461 || NET462 + return Task.FromResult(false); +#else + return Task.CompletedTask; +#endif + } + + return Task.Delay(delay, cancellationToken); + } + + public SystemThreading.CancellationTokenSource CreateCancellationTokenSource(TimeSpan delay) + { + if (delay < TimeSpan.Zero && delay != s_infiniteTimeSpan) + throw new ArgumentOutOfRangeException(nameof(delay)); + + if (delay == s_infiniteTimeSpan) + { + return new SystemThreading.CancellationTokenSource(); + } + + return new SystemThreading.CancellationTokenSource(delay); + } + + private sealed class SystemTimeProvider : TimeProvider + { + public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Local; + } + + private sealed class SystemTimeProviderTimer : SystemThreading.ITimer + { + private readonly SystemThreading.Timer _timer; + + 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(); +#else + _timer.Dispose(); + return default; +#endif + } +#elif FEATURE_VALUETASK + public ValueTask DisposeAsync() + { + _timer.Dispose(); + return default; + } +#endif + } +} +#endif +#endif diff --git a/PolyShim/PolyShim.csproj b/PolyShim/PolyShim.csproj index a8705455..fae85b18 100644 --- a/PolyShim/PolyShim.csproj +++ b/PolyShim/PolyShim.csproj @@ -50,19 +50,19 @@ @@ -144,6 +144,24 @@ PrivateAssets="all" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework' AND $([MSBuild]::VersionEquals($(TargetFrameworkVersion), '4.0'))" /> + + + diff --git a/PolyShim/PolyShim.targets b/PolyShim/PolyShim.targets index 6360611d..67077ac5 100644 --- a/PolyShim/PolyShim.targets +++ b/PolyShim/PolyShim.targets @@ -302,6 +302,23 @@ $(DefineConstants);FEATURE_VALUETUPLE + + false + true + true + true + $(DefineConstants);FEATURE_TIMEPROVIDER diff --git a/PolyShim/Signatures.md b/PolyShim/Signatures.md index d5c8212a..fb64f466 100644 --- a/PolyShim/Signatures.md +++ b/PolyShim/Signatures.md @@ -1,7 +1,7 @@ # Signatures -- **Total:** 355 -- **Types:** 76 +- **Total:** 357 +- **Types:** 78 - **Members:** 279 ___ @@ -187,6 +187,8 @@ ___ - [`TValue? GetValueOrDefault(TKey)`](https://learn.microsoft.com/dotnet/api/system.collections.generic.collectionextensions.getvalueordefault#system-collections-generic-collectionextensions-getvalueordefault-2(system-collections-generic-ireadonlydictionary((-0-1))-0)) .NET Core 2.0 - `IsExternalInit` - [**[class]**](https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.isexternalinit) .NET 5.0 +- `ITimer` + - [**[interface]**](https://learn.microsoft.com/dotnet/api/system.threading.itimer) .NET 8.0 - `KeyValuePair` - [`void Deconstruct(out TKey, out TValue)`](https://learn.microsoft.com/dotnet/api/system.collections.generic.keyvaluepair-2.deconstruct) .NET Core 2.0 - `LibraryImportAttribute` @@ -441,6 +443,8 @@ ___ - [`void Write(ReadOnlySpan)`](https://learn.microsoft.com/dotnet/api/system.io.textwriter.write#system-io-textwriter-write(system-readonlyspan((system-char)))) .NET Core 2.1 - `ThreadAbortException` - [**[class]**](https://learn.microsoft.com/dotnet/api/system.threading.threadabortexception) .NET Core 2.0 +- `TimeProvider` + - [**[class]**](https://learn.microsoft.com/dotnet/api/system.timeprovider) .NET 8.0 - `TimeSpan` - [`TimeSpan FromMilliseconds(long, long)`](https://learn.microsoft.com/dotnet/api/system.timespan.frommilliseconds#system-timespan-frommilliseconds(system-int64-system-int64)) .NET 9.0 - [`TimeSpan FromMilliseconds(long)`](https://learn.microsoft.com/dotnet/api/system.timespan.frommilliseconds#system-timespan-frommilliseconds(system-int64)) .NET 10.0 From 01cac72d0aaea1ae0ac55bce918aa924e60a4bbe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 20:45:23 +0000 Subject: [PATCH 03/15] Update README to mention Microsoft.Bcl.TimeProvider compatibility package Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- Readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Readme.md b/Readme.md index 747e512d..31209d7e 100644 --- a/Readme.md +++ b/Readme.md @@ -193,6 +193,7 @@ Currently, **PolyShim** recognizes the following packages: - [`Microsoft.Bcl.AsyncInterfaces`](https://nuget.org/packages/Microsoft.Bcl.AsyncInterfaces) — `IAsyncEnumerable`, `IAsyncDisposable`, etc. - [`Microsoft.Bcl.HashCode`](https://nuget.org/packages/Microsoft.Bcl.HashCode) — `HashCode`, etc. - [`Microsoft.Bcl.Memory`](https://nuget.org/packages/Microsoft.Bcl.Memory) — `Index`, `Range`, etc. +- [`Microsoft.Bcl.TimeProvider`](https://nuget.org/packages/Microsoft.Bcl.TimeProvider) — `TimeProvider`, `ITimer`, etc. - [`Microsoft.Net.Http`](https://nuget.org/packages/Microsoft.Net.Http) — `HttpClient`, `HttpContent`, etc. (wider support than the `System.*` variant). For example, adding a reference to the `Microsoft.Bcl.AsyncInterfaces` package will enable **PolyShim**'s polyfills that work with `IAsyncEnumerable`, such as `Task.WhenEach(...)`: From a4b359034faedd758e3220ad166231f62286752d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:14:02 +0000 Subject: [PATCH 04/15] Address code review feedback: simplify conditionals, remove guards, use polyfilled APIs Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- PolyShim/Net80/ITimer.cs | 9 ++--- PolyShim/Net80/TimeProvider.cs | 63 +++++++++++++++------------------- 2 files changed, 30 insertions(+), 42 deletions(-) diff --git a/PolyShim/Net80/ITimer.cs b/PolyShim/Net80/ITimer.cs index b3967850..524c7ea6 100644 --- a/PolyShim/Net80/ITimer.cs +++ b/PolyShim/Net80/ITimer.cs @@ -1,6 +1,4 @@ -#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) +#if !FEATURE_TIMEPROVIDER #nullable enable // ReSharper disable RedundantUsingDirective // ReSharper disable CheckNamespace @@ -8,7 +6,6 @@ // ReSharper disable PartialTypeWithSinglePart using System; -using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; namespace System.Threading; @@ -16,11 +13,9 @@ namespace System.Threading; // https://learn.microsoft.com/dotnet/api/system.threading.itimer internal interface ITimer : IDisposable #if FEATURE_ASYNCINTERFACES - , - IAsyncDisposable + , IAsyncDisposable #endif { bool Change(TimeSpan dueTime, TimeSpan period); } #endif -#endif diff --git a/PolyShim/Net80/TimeProvider.cs b/PolyShim/Net80/TimeProvider.cs index 3ff185c2..90b49a4f 100644 --- a/PolyShim/Net80/TimeProvider.cs +++ b/PolyShim/Net80/TimeProvider.cs @@ -1,7 +1,4 @@ -#if !FEATURE_TIMEPROVIDER && ((NETCOREAPP && !NET8_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD)) -// 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) +#if !FEATURE_TIMEPROVIDER #nullable enable // ReSharper disable RedundantUsingDirective // ReSharper disable CheckNamespace @@ -11,8 +8,10 @@ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; using SystemThreading = System.Threading; +#if FEATURE_TASK +using System.Threading.Tasks; +#endif namespace System; @@ -21,7 +20,6 @@ namespace System; internal abstract class TimeProvider { private static readonly TimeProvider s_system = new SystemTimeProvider(); - private static readonly TimeSpan s_infiniteTimeSpan = SystemThreading.Timeout.InfiniteTimeSpan; public static TimeProvider System => s_system; @@ -65,68 +63,69 @@ public virtual TimeSpan GetElapsedTime(long startingTimestamp, long endingTimest } } +#if !NETSTANDARD1_0 && !NETSTANDARD1_1 public SystemThreading.ITimer CreateTimer( SystemThreading.TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period - ) - { - if (callback is null) - throw new ArgumentNullException(nameof(callback)); - - return new SystemTimeProviderTimer(dueTime, period, callback, state); - } + ) => new SystemTimeProviderTimer(dueTime, period, callback, state); +#endif +#if FEATURE_TASK public virtual Task Delay( TimeSpan delay, SystemThreading.CancellationToken cancellationToken = default ) { - if (delay < TimeSpan.Zero && delay != s_infiniteTimeSpan) +#if NETSTANDARD1_3_OR_GREATER || NETCOREAPP || NET45_OR_GREATER + if (delay < TimeSpan.Zero && delay != SystemThreading.Timeout.InfiniteTimeSpan) throw new ArgumentOutOfRangeException(nameof(delay)); if (cancellationToken.IsCancellationRequested) { -#if NETCOREAPP || NETSTANDARD2_1_OR_GREATER - return Task.FromCanceled(cancellationToken); -#else var tcs = new TaskCompletionSource(); tcs.SetCanceled(); return tcs.Task; -#endif } if (delay == TimeSpan.Zero) - { -#if NETSTANDARD2_0 || NET461 || NET462 - return Task.FromResult(false); -#else return Task.CompletedTask; -#endif - } return Task.Delay(delay, cancellationToken); +#else + // Task.Delay and Timeout.InfiniteTimeSpan are not available on older TFMs + throw new NotSupportedException( + "Delay is not supported on this target framework. Use the Microsoft.Bcl.TimeProvider package instead." + ); +#endif } public SystemThreading.CancellationTokenSource CreateCancellationTokenSource(TimeSpan delay) { - if (delay < TimeSpan.Zero && delay != s_infiniteTimeSpan) +#if NETSTANDARD1_3_OR_GREATER || NETCOREAPP || NET45_OR_GREATER + if (delay < TimeSpan.Zero && delay != SystemThreading.Timeout.InfiniteTimeSpan) throw new ArgumentOutOfRangeException(nameof(delay)); - if (delay == s_infiniteTimeSpan) - { + if (delay == SystemThreading.Timeout.InfiniteTimeSpan) return new SystemThreading.CancellationTokenSource(); - } return new SystemThreading.CancellationTokenSource(delay); +#else + // CancellationTokenSource(TimeSpan) is not available on older TFMs + throw new NotSupportedException( + "CreateCancellationTokenSource is not supported on this target framework. Use the Microsoft.Bcl.TimeProvider package instead." + ); +#endif } +#endif private sealed class SystemTimeProvider : TimeProvider { public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Local; } +#if !NETSTANDARD1_0 && !NETSTANDARD1_1 private sealed class SystemTimeProviderTimer : SystemThreading.ITimer { private readonly SystemThreading.Timer _timer; @@ -168,14 +167,8 @@ public ValueTask DisposeAsync() return default; #endif } -#elif FEATURE_VALUETASK - public ValueTask DisposeAsync() - { - _timer.Dispose(); - return default; - } #endif } -} #endif +} #endif From 65710d0d5ea678fb361e3ae94cff80a73bd91054 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:26:23 +0000 Subject: [PATCH 05/15] Address review feedback: remove conditional for Tasks using, inline System property, add comments, fix conditionals Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- PolyShim/Net80/TimeProvider.cs | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/PolyShim/Net80/TimeProvider.cs b/PolyShim/Net80/TimeProvider.cs index 90b49a4f..78c944e6 100644 --- a/PolyShim/Net80/TimeProvider.cs +++ b/PolyShim/Net80/TimeProvider.cs @@ -8,10 +8,8 @@ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using SystemThreading = System.Threading; -#if FEATURE_TASK using System.Threading.Tasks; -#endif +using SystemThreading = System.Threading; namespace System; @@ -19,9 +17,7 @@ namespace System; [ExcludeFromCodeCoverage] internal abstract class TimeProvider { - private static readonly TimeProvider s_system = new SystemTimeProvider(); - - public static TimeProvider System => s_system; + public static TimeProvider System { get; } = new SystemTimeProvider(); protected TimeProvider() { } @@ -63,7 +59,8 @@ public virtual TimeSpan GetElapsedTime(long startingTimestamp, long endingTimest } } -#if !NETSTANDARD1_0 && !NETSTANDARD1_1 + // Timer and TimerCallback are not available on .NET Standard 1.0 and 1.1 +#if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) public SystemThreading.ITimer CreateTimer( SystemThreading.TimerCallback callback, object? state, @@ -72,13 +69,13 @@ TimeSpan period ) => new SystemTimeProviderTimer(dueTime, period, callback, state); #endif -#if FEATURE_TASK + // Task.Delay, Timeout.InfiniteTimeSpan, and CancellationTokenSource(TimeSpan) require .NET 4.5+ +#if FEATURE_TASK && (NETSTANDARD1_3_OR_GREATER || NETCOREAPP || NET45_OR_GREATER) public virtual Task Delay( TimeSpan delay, SystemThreading.CancellationToken cancellationToken = default ) { -#if NETSTANDARD1_3_OR_GREATER || NETCOREAPP || NET45_OR_GREATER if (delay < TimeSpan.Zero && delay != SystemThreading.Timeout.InfiniteTimeSpan) throw new ArgumentOutOfRangeException(nameof(delay)); @@ -93,17 +90,10 @@ public virtual Task Delay( return Task.CompletedTask; return Task.Delay(delay, cancellationToken); -#else - // Task.Delay and Timeout.InfiniteTimeSpan are not available on older TFMs - throw new NotSupportedException( - "Delay is not supported on this target framework. Use the Microsoft.Bcl.TimeProvider package instead." - ); -#endif } public SystemThreading.CancellationTokenSource CreateCancellationTokenSource(TimeSpan delay) { -#if NETSTANDARD1_3_OR_GREATER || NETCOREAPP || NET45_OR_GREATER if (delay < TimeSpan.Zero && delay != SystemThreading.Timeout.InfiniteTimeSpan) throw new ArgumentOutOfRangeException(nameof(delay)); @@ -111,12 +101,6 @@ public SystemThreading.CancellationTokenSource CreateCancellationTokenSource(Tim return new SystemThreading.CancellationTokenSource(); return new SystemThreading.CancellationTokenSource(delay); -#else - // CancellationTokenSource(TimeSpan) is not available on older TFMs - throw new NotSupportedException( - "CreateCancellationTokenSource is not supported on this target framework. Use the Microsoft.Bcl.TimeProvider package instead." - ); -#endif } #endif @@ -125,7 +109,8 @@ private sealed class SystemTimeProvider : TimeProvider public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Local; } -#if !NETSTANDARD1_0 && !NETSTANDARD1_1 + // 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; From be2f69e907f32ab93c48258e3610eec084c66d0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:42:59 +0000 Subject: [PATCH 06/15] Address review: add polyfills, reorganize properties, remove guards, simplify code Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- PolyShim/Net45/Task.cs | 60 ++++++++++++++++++ PolyShim/Net70/Stopwatch.cs | 44 ++++++++++++++ PolyShim/Net80/TimeProvider.cs | 81 ++++++++----------------- PolyShim/NetCore30/Timer.cs | 25 ++++++++ PolyShim/NetStandard12/TimerCallback.cs | 12 ++++ PolyShim/Signatures.md | 11 +++- 6 files changed, 175 insertions(+), 58 deletions(-) create mode 100644 PolyShim/Net45/Task.cs create mode 100644 PolyShim/Net70/Stopwatch.cs create mode 100644 PolyShim/NetCore30/Timer.cs create mode 100644 PolyShim/NetStandard12/TimerCallback.cs diff --git a/PolyShim/Net45/Task.cs b/PolyShim/Net45/Task.cs new file mode 100644 index 00000000..089039c5 --- /dev/null +++ b/PolyShim/Net45/Task.cs @@ -0,0 +1,60 @@ +#if FEATURE_TASK && !NET45_OR_GREATER && !NETSTANDARD1_3_OR_GREATER && !NETCOREAPP +// Timer is required for Task.Delay implementation +#if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) +#nullable enable +// ReSharper disable RedundantUsingDirective +// ReSharper disable CheckNamespace +// ReSharper disable InconsistentNaming +// ReSharper disable PartialTypeWithSinglePart + +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; + +[ExcludeFromCodeCoverage] +internal static class MemberPolyfills_Net45_Task +{ + extension(Task) + { + // 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(); + + if (cancellationToken.IsCancellationRequested) + { + tcs.SetCanceled(); + return tcs.Task; + } + + var timer = new Timer( + _ => + { + tcs.TrySetResult(true); + }, + null, + delay, + TimeSpan.FromMilliseconds(-1) + ); + + cancellationToken.Register(() => + { + timer.Dispose(); + tcs.TrySetCanceled(); + }); + + tcs.Task.ContinueWith( + _ => timer.Dispose(), + TaskContinuationOptions.ExecuteSynchronously + ); + + 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 diff --git a/PolyShim/Net70/Stopwatch.cs b/PolyShim/Net70/Stopwatch.cs new file mode 100644 index 00000000..4fd34197 --- /dev/null +++ b/PolyShim/Net70/Stopwatch.cs @@ -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 diff --git a/PolyShim/Net80/TimeProvider.cs b/PolyShim/Net80/TimeProvider.cs index 78c944e6..e4e65c9e 100644 --- a/PolyShim/Net80/TimeProvider.cs +++ b/PolyShim/Net80/TimeProvider.cs @@ -8,8 +8,8 @@ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Threading; using System.Threading.Tasks; -using SystemThreading = System.Threading; namespace System; @@ -21,6 +21,8 @@ internal abstract class TimeProvider protected TimeProvider() { } + public abstract TimeZoneInfo LocalTimeZone { get; } + public virtual DateTimeOffset GetUtcNow() => DateTimeOffset.UtcNow; public DateTimeOffset GetLocalNow() @@ -30,55 +32,26 @@ public DateTimeOffset GetLocalNow() return new DateTimeOffset(utcDateTime.DateTime + offset, offset); } - public abstract TimeZoneInfo LocalTimeZone { get; } - public virtual long GetTimestamp() => Stopwatch.GetTimestamp(); public TimeSpan GetElapsedTime(long startingTimestamp) => 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 - var tickFrequency = Stopwatch.Frequency; - var ticks = endingTimestamp - startingTimestamp; + public virtual TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) => + Stopwatch.GetElapsedTime(startingTimestamp, endingTimestamp); - 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 #if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) - public SystemThreading.ITimer CreateTimer( - SystemThreading.TimerCallback callback, + public ITimer CreateTimer( + 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+ -#if FEATURE_TASK && (NETSTANDARD1_3_OR_GREATER || NETCOREAPP || NET45_OR_GREATER) - public virtual Task Delay( - TimeSpan delay, - SystemThreading.CancellationToken cancellationToken = default - ) +#if FEATURE_TASK + public virtual Task Delay(TimeSpan delay, CancellationToken cancellationToken = default) { - if (delay < TimeSpan.Zero && delay != SystemThreading.Timeout.InfiniteTimeSpan) - throw new ArgumentOutOfRangeException(nameof(delay)); - if (cancellationToken.IsCancellationRequested) { var tcs = new TaskCompletionSource(); @@ -92,15 +65,20 @@ public virtual Task Delay( return Task.Delay(delay, cancellationToken); } - public SystemThreading.CancellationTokenSource CreateCancellationTokenSource(TimeSpan delay) + public CancellationTokenSource CreateCancellationTokenSource(TimeSpan delay) { - if (delay < TimeSpan.Zero && delay != SystemThreading.Timeout.InfiniteTimeSpan) - throw new ArgumentOutOfRangeException(nameof(delay)); + var infiniteTimeSpan = TimeSpan.FromMilliseconds(-1); - if (delay == SystemThreading.Timeout.InfiniteTimeSpan) - return new SystemThreading.CancellationTokenSource(); + if (delay == infiniteTimeSpan) + return new CancellationTokenSource(); - return new SystemThreading.CancellationTokenSource(delay); +#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 } #endif @@ -109,20 +87,19 @@ 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 sealed class SystemTimeProviderTimer : ITimer { - private readonly SystemThreading.Timer _timer; + private readonly Timer _timer; public SystemTimeProviderTimer( TimeSpan dueTime, TimeSpan period, - SystemThreading.TimerCallback callback, + TimerCallback callback, object? state ) { - _timer = new SystemThreading.Timer(callback, state, dueTime, period); + _timer = new Timer(callback, state, dueTime, period); } public bool Change(TimeSpan dueTime, TimeSpan period) @@ -143,15 +120,7 @@ public void Dispose() } #if FEATURE_ASYNCINTERFACES - public ValueTask DisposeAsync() - { -#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER - return _timer.DisposeAsync(); -#else - _timer.Dispose(); - return default; -#endif - } + public ValueTask DisposeAsync() => _timer.DisposeAsync(); #endif } #endif diff --git a/PolyShim/NetCore30/Timer.cs b/PolyShim/NetCore30/Timer.cs new file mode 100644 index 00000000..25c78c2c --- /dev/null +++ b/PolyShim/NetCore30/Timer.cs @@ -0,0 +1,25 @@ +#if FEATURE_ASYNCINTERFACES && !(NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER) +#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) + { + // https://learn.microsoft.com/dotnet/api/system.threading.timer.disposeasync + public ValueTask DisposeAsync() + { + timer.Dispose(); + return default; + } + } +} +#endif diff --git a/PolyShim/NetStandard12/TimerCallback.cs b/PolyShim/NetStandard12/TimerCallback.cs new file mode 100644 index 00000000..e6cef73a --- /dev/null +++ b/PolyShim/NetStandard12/TimerCallback.cs @@ -0,0 +1,12 @@ +#if NETSTANDARD && !NETSTANDARD1_2_OR_GREATER +#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 diff --git a/PolyShim/Signatures.md b/PolyShim/Signatures.md index fb64f466..6f7708ca 100644 --- a/PolyShim/Signatures.md +++ b/PolyShim/Signatures.md @@ -1,8 +1,8 @@ # Signatures -- **Total:** 357 +- **Total:** 362 - **Types:** 78 -- **Members:** 279 +- **Members:** 284 ___ @@ -336,6 +336,9 @@ ___ - [`bool SequenceEqual(ReadOnlySpan)`](https://learn.microsoft.com/dotnet/api/system.memoryextensions.sequenceequal#system-memoryextensions-sequenceequal-1(system-span((-0))-system-readonlyspan((-0)))) .NET Core 2.1 - [`int IndexOf(T)`](https://learn.microsoft.com/dotnet/api/system.memoryextensions.indexof#system-memoryextensions-indexof-1(system-span((-0))-0)) .NET Core 2.1 - [`void Reverse()`](https://learn.microsoft.com/dotnet/api/system.memoryextensions.reverse#system-memoryextensions-reverse-1(system-span((-0)))) .NET Core 2.1 +- `Stopwatch` + - [`TimeSpan GetElapsedTime(long, long)`](https://learn.microsoft.com/dotnet/api/system.diagnostics.stopwatch.getelapsedtime#system-diagnostics-stopwatch-getelapsedtime(system-int64-system-int64)) .NET 7.0 + - [`TimeSpan GetElapsedTime(long)`](https://learn.microsoft.com/dotnet/api/system.diagnostics.stopwatch.getelapsedtime#system-diagnostics-stopwatch-getelapsedtime(system-int64)) .NET 7.0 - `Stream` - [`int Read(Span)`](https://learn.microsoft.com/dotnet/api/system.io.stream.read#system-io-stream-read(system-span((system-byte)))) .NET Core 2.1 - [`int ReadAtLeast(Span, int, bool)`](https://learn.microsoft.com/dotnet/api/system.io.stream.readatleast) .NET 7.0 @@ -403,6 +406,8 @@ ___ - [`IAsyncEnumerable WhenEach(IEnumerable)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.wheneach#system-threading-tasks-task-wheneach(system-collections-generic-ienumerable((system-threading-tasks-task)))) .NET 9.0 - [`IAsyncEnumerable WhenEach(params Task[])`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.wheneach#system-threading-tasks-task-wheneach(system-threading-tasks-task())) .NET 9.0 - [`Task CompletedTask`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.completedtask) .NET Core 1.0 + - [`Task Delay(TimeSpan, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.delay#system-threading-tasks-task-delay(system-timespan-system-threading-cancellationtoken)) .NET 4.5 + - [`Task Delay(TimeSpan)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.delay#system-threading-tasks-task-delay(system-timespan)) .NET 4.5 - [`Task Run(Action, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.run#system-threading-tasks-task-run(system-action-system-threading-cancellationtoken)) .NET Core 1.0 - [`Task Run(Action)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.run#system-threading-tasks-task-run(system-action)) .NET Core 1.0 - [`Task Run(Func, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.run#system-threading-tasks-task-run(system-func((system-threading-tasks-task))-system-threading-cancellationtoken)) .NET Core 1.0 @@ -445,6 +450,8 @@ ___ - [**[class]**](https://learn.microsoft.com/dotnet/api/system.threading.threadabortexception) .NET Core 2.0 - `TimeProvider` - [**[class]**](https://learn.microsoft.com/dotnet/api/system.timeprovider) .NET 8.0 +- `Timer` + - [`ValueTask DisposeAsync()`](https://learn.microsoft.com/dotnet/api/system.threading.timer.disposeasync) .NET Core 3.0 - `TimeSpan` - [`TimeSpan FromMilliseconds(long, long)`](https://learn.microsoft.com/dotnet/api/system.timespan.frommilliseconds#system-timespan-frommilliseconds(system-int64-system-int64)) .NET 9.0 - [`TimeSpan FromMilliseconds(long)`](https://learn.microsoft.com/dotnet/api/system.timespan.frommilliseconds#system-timespan-frommilliseconds(system-int64)) .NET 10.0 From a32247672e0cb7bfe43e450991ad5b3f1a4a2f83 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:59:44 +0000 Subject: [PATCH 07/15] Address review: fix timer disposal, use CancelAfter, move guards to members Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- PolyShim/Net45/Task.cs | 16 ++++++---------- PolyShim/Net80/TimeProvider.cs | 15 ++++++++------- PolyShim/NetCore30/Timer.cs | 6 +++++- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/PolyShim/Net45/Task.cs b/PolyShim/Net45/Task.cs index 089039c5..380c38a5 100644 --- a/PolyShim/Net45/Task.cs +++ b/PolyShim/Net45/Task.cs @@ -1,6 +1,4 @@ #if FEATURE_TASK && !NET45_OR_GREATER && !NETSTANDARD1_3_OR_GREATER && !NETCOREAPP -// Timer is required for Task.Delay implementation -#if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) #nullable enable // ReSharper disable RedundantUsingDirective // ReSharper disable CheckNamespace @@ -17,6 +15,7 @@ internal static class MemberPolyfills_Net45_Task { extension(Task) { +#if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) // 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) { @@ -28,9 +27,11 @@ public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) return tcs.Task; } - var timer = new Timer( + Timer? timer = null; + timer = new Timer( _ => { + timer?.Dispose(); tcs.TrySetResult(true); }, null, @@ -40,21 +41,16 @@ public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) cancellationToken.Register(() => { - timer.Dispose(); + timer?.Dispose(); tcs.TrySetCanceled(); }); - tcs.Task.ContinueWith( - _ => timer.Dispose(), - TaskContinuationOptions.ExecuteSynchronously - ); - 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 -#endif diff --git a/PolyShim/Net80/TimeProvider.cs b/PolyShim/Net80/TimeProvider.cs index e4e65c9e..1f8eeaa8 100644 --- a/PolyShim/Net80/TimeProvider.cs +++ b/PolyShim/Net80/TimeProvider.cs @@ -68,17 +68,18 @@ public virtual Task Delay(TimeSpan delay, CancellationToken cancellationToken = public CancellationTokenSource CreateCancellationTokenSource(TimeSpan delay) { var infiniteTimeSpan = TimeSpan.FromMilliseconds(-1); + var cts = new CancellationTokenSource(); - if (delay == infiniteTimeSpan) - return new CancellationTokenSource(); - + if (delay != infiniteTimeSpan) + { #if NET45_OR_GREATER || NETSTANDARD1_3_OR_GREATER || NETCOREAPP - return new CancellationTokenSource(delay); + cts.CancelAfter(delay); #else - // CancellationTokenSource(int) constructor added in .NET 4.5 - // For older frameworks, just return a plain instance - return new CancellationTokenSource(); + cts.CancelAfter((int)delay.TotalMilliseconds); #endif + } + + return cts; } #endif diff --git a/PolyShim/NetCore30/Timer.cs b/PolyShim/NetCore30/Timer.cs index 25c78c2c..f27b1131 100644 --- a/PolyShim/NetCore30/Timer.cs +++ b/PolyShim/NetCore30/Timer.cs @@ -1,4 +1,5 @@ -#if FEATURE_ASYNCINTERFACES && !(NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER) +#if (NETCOREAPP && !NETCOREAPP3_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD && !NETSTANDARD2_1_OR_GREATER) +#if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) #nullable enable // ReSharper disable RedundantUsingDirective // ReSharper disable CheckNamespace @@ -14,12 +15,15 @@ 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 From f7dfa0d620915c7f0ad08046655d6f66be0fbaad Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:29:15 +0000 Subject: [PATCH 08/15] Address review: separate conditionals, add comments, fix registration disposal, use FromCanceled, simplify logic Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- PolyShim/Net45/Task.cs | 12 ++++++++++-- PolyShim/Net46/Task.cs | 28 ++++++++++++++++++++++++++++ PolyShim/Net80/TimeProvider.cs | 21 +++++---------------- PolyShim/NetCore30/Timer.cs | 1 + PolyShim/Signatures.md | 5 +++-- 5 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 PolyShim/Net46/Task.cs diff --git a/PolyShim/Net45/Task.cs b/PolyShim/Net45/Task.cs index 380c38a5..1d88c6e1 100644 --- a/PolyShim/Net45/Task.cs +++ b/PolyShim/Net45/Task.cs @@ -1,4 +1,5 @@ -#if FEATURE_TASK && !NET45_OR_GREATER && !NETSTANDARD1_3_OR_GREATER && !NETCOREAPP +#if FEATURE_TASK +#if (NETFRAMEWORK && !NET45_OR_GREATER) || (NETSTANDARD && !NETSTANDARD1_3_OR_GREATER) #nullable enable // ReSharper disable RedundantUsingDirective // ReSharper disable CheckNamespace @@ -15,6 +16,7 @@ internal static class MemberPolyfills_Net45_Task { extension(Task) { + // Timer is not available on .NET Standard 1.0 and 1.1 #if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) // 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) @@ -39,12 +41,17 @@ public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) TimeSpan.FromMilliseconds(-1) ); - cancellationToken.Register(() => + var registration = cancellationToken.Register(() => { timer?.Dispose(); tcs.TrySetCanceled(); }); + tcs.Task.ContinueWith( + _ => registration.Dispose(), + TaskContinuationOptions.ExecuteSynchronously + ); + return tcs.Task; } @@ -54,3 +61,4 @@ public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) } } #endif +#endif diff --git a/PolyShim/Net46/Task.cs b/PolyShim/Net46/Task.cs new file mode 100644 index 00000000..08b6dfd3 --- /dev/null +++ b/PolyShim/Net46/Task.cs @@ -0,0 +1,28 @@ +#if FEATURE_TASK +#if (NETFRAMEWORK && !NET46_OR_GREATER) || (NETSTANDARD && !NETSTANDARD2_1_OR_GREATER) +#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_Net46_Task +{ + extension(Task) + { + // https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.fromcanceled + public static Task FromCanceled(CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + tcs.SetCanceled(); + return tcs.Task; + } + } +} +#endif +#endif diff --git a/PolyShim/Net80/TimeProvider.cs b/PolyShim/Net80/TimeProvider.cs index 1f8eeaa8..9c68a24c 100644 --- a/PolyShim/Net80/TimeProvider.cs +++ b/PolyShim/Net80/TimeProvider.cs @@ -34,12 +34,12 @@ public DateTimeOffset GetLocalNow() public virtual long GetTimestamp() => Stopwatch.GetTimestamp(); - public TimeSpan GetElapsedTime(long startingTimestamp) => - GetElapsedTime(startingTimestamp, 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 ITimer CreateTimer( TimerCallback callback, @@ -53,11 +53,7 @@ TimeSpan period public virtual Task Delay(TimeSpan delay, CancellationToken cancellationToken = default) { if (cancellationToken.IsCancellationRequested) - { - var tcs = new TaskCompletionSource(); - tcs.SetCanceled(); - return tcs.Task; - } + return Task.FromCanceled(cancellationToken); if (delay == TimeSpan.Zero) return Task.CompletedTask; @@ -67,17 +63,10 @@ public virtual Task Delay(TimeSpan delay, CancellationToken cancellationToken = public CancellationTokenSource CreateCancellationTokenSource(TimeSpan delay) { - var infiniteTimeSpan = TimeSpan.FromMilliseconds(-1); var cts = new CancellationTokenSource(); - if (delay != infiniteTimeSpan) - { -#if NET45_OR_GREATER || NETSTANDARD1_3_OR_GREATER || NETCOREAPP + if (delay != TimeSpan.Zero && delay.Ticks > 0) cts.CancelAfter(delay); -#else - cts.CancelAfter((int)delay.TotalMilliseconds); -#endif - } return cts; } diff --git a/PolyShim/NetCore30/Timer.cs b/PolyShim/NetCore30/Timer.cs index f27b1131..7252a342 100644 --- a/PolyShim/NetCore30/Timer.cs +++ b/PolyShim/NetCore30/Timer.cs @@ -1,3 +1,4 @@ +// Timer is not available on .NET Standard 1.0 and 1.1 #if (NETCOREAPP && !NETCOREAPP3_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD && !NETSTANDARD2_1_OR_GREATER) #if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) #nullable enable diff --git a/PolyShim/Signatures.md b/PolyShim/Signatures.md index 6f7708ca..38bd49c8 100644 --- a/PolyShim/Signatures.md +++ b/PolyShim/Signatures.md @@ -1,8 +1,8 @@ # Signatures -- **Total:** 362 +- **Total:** 363 - **Types:** 78 -- **Members:** 284 +- **Members:** 285 ___ @@ -408,6 +408,7 @@ ___ - [`Task CompletedTask`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.completedtask) .NET Core 1.0 - [`Task Delay(TimeSpan, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.delay#system-threading-tasks-task-delay(system-timespan-system-threading-cancellationtoken)) .NET 4.5 - [`Task Delay(TimeSpan)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.delay#system-threading-tasks-task-delay(system-timespan)) .NET 4.5 + - [`Task FromCanceled(CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.fromcanceled) .NET 4.6 - [`Task Run(Action, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.run#system-threading-tasks-task-run(system-action-system-threading-cancellationtoken)) .NET Core 1.0 - [`Task Run(Action)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.run#system-threading-tasks-task-run(system-action)) .NET Core 1.0 - [`Task Run(Func, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.run#system-threading-tasks-task-run(system-func((system-threading-tasks-task))-system-threading-cancellationtoken)) .NET Core 1.0 From c1b7ce50409e9c183b13c5b80ed6168877e3aa51 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:47:52 +0000 Subject: [PATCH 09/15] Fix folder naming: move polyfills to NetCore10, dispose timer in ContinueWith, pass cancellation token, move comment Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- PolyShim/Net45/Task.cs | 64 ------------------------------------- PolyShim/Net46/Task.cs | 28 ---------------- PolyShim/NetCore10/Task.cs | 55 +++++++++++++++++++++++++++++++ PolyShim/NetCore30/Timer.cs | 2 +- PolyShim/Signatures.md | 6 ++-- 5 files changed, 59 insertions(+), 96 deletions(-) delete mode 100644 PolyShim/Net45/Task.cs delete mode 100644 PolyShim/Net46/Task.cs diff --git a/PolyShim/Net45/Task.cs b/PolyShim/Net45/Task.cs deleted file mode 100644 index 1d88c6e1..00000000 --- a/PolyShim/Net45/Task.cs +++ /dev/null @@ -1,64 +0,0 @@ -#if FEATURE_TASK -#if (NETFRAMEWORK && !NET45_OR_GREATER) || (NETSTANDARD && !NETSTANDARD1_3_OR_GREATER) -#nullable enable -// ReSharper disable RedundantUsingDirective -// ReSharper disable CheckNamespace -// ReSharper disable InconsistentNaming -// ReSharper disable PartialTypeWithSinglePart - -using System; -using System.Threading; -using System.Threading.Tasks; -using System.Diagnostics.CodeAnalysis; - -[ExcludeFromCodeCoverage] -internal static class MemberPolyfills_Net45_Task -{ - extension(Task) - { - // Timer is not available on .NET Standard 1.0 and 1.1 -#if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) - // 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(); - - if (cancellationToken.IsCancellationRequested) - { - tcs.SetCanceled(); - return tcs.Task; - } - - Timer? timer = null; - timer = new Timer( - _ => - { - timer?.Dispose(); - tcs.TrySetResult(true); - }, - null, - delay, - TimeSpan.FromMilliseconds(-1) - ); - - var registration = cancellationToken.Register(() => - { - timer?.Dispose(); - tcs.TrySetCanceled(); - }); - - tcs.Task.ContinueWith( - _ => registration.Dispose(), - TaskContinuationOptions.ExecuteSynchronously - ); - - 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 -#endif diff --git a/PolyShim/Net46/Task.cs b/PolyShim/Net46/Task.cs deleted file mode 100644 index 08b6dfd3..00000000 --- a/PolyShim/Net46/Task.cs +++ /dev/null @@ -1,28 +0,0 @@ -#if FEATURE_TASK -#if (NETFRAMEWORK && !NET46_OR_GREATER) || (NETSTANDARD && !NETSTANDARD2_1_OR_GREATER) -#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_Net46_Task -{ - extension(Task) - { - // https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.fromcanceled - public static Task FromCanceled(CancellationToken cancellationToken) - { - var tcs = new TaskCompletionSource(); - tcs.SetCanceled(); - return tcs.Task; - } - } -} -#endif -#endif diff --git a/PolyShim/NetCore10/Task.cs b/PolyShim/NetCore10/Task.cs index b164e74e..f0a87c3b 100644 --- a/PolyShim/NetCore10/Task.cs +++ b/PolyShim/NetCore10/Task.cs @@ -150,6 +150,61 @@ public static Task> WhenAny(IEnumerable> tasks) => public static Task> WhenAny(params Task[] tasks) => WhenAny((IEnumerable>)tasks); #endif + + // Timer is not available on .NET Standard 1.0 and 1.1 +#if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) + // 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(); + + if (cancellationToken.IsCancellationRequested) + { + tcs.SetCanceled(); + return tcs.Task; + } + + Timer? timer = null; + timer = new Timer( + _ => + { + timer?.Dispose(); + tcs.TrySetResult(true); + }, + null, + delay, + TimeSpan.FromMilliseconds(-1) + ); + + var registration = cancellationToken.Register(() => + { + timer?.Dispose(); + tcs.TrySetCanceled(); + }); + + tcs.Task.ContinueWith( + _ => + { + registration.Dispose(); + timer?.Dispose(); + }, + TaskContinuationOptions.ExecuteSynchronously + ); + + 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 + + // https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.fromcanceled + public static Task FromCanceled(CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + tcs.TrySetCanceled(cancellationToken); + return tcs.Task; + } } } #endif diff --git a/PolyShim/NetCore30/Timer.cs b/PolyShim/NetCore30/Timer.cs index 7252a342..b2ae9611 100644 --- a/PolyShim/NetCore30/Timer.cs +++ b/PolyShim/NetCore30/Timer.cs @@ -1,5 +1,5 @@ -// Timer is not available on .NET Standard 1.0 and 1.1 #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) #nullable enable // ReSharper disable RedundantUsingDirective diff --git a/PolyShim/Signatures.md b/PolyShim/Signatures.md index 38bd49c8..82643630 100644 --- a/PolyShim/Signatures.md +++ b/PolyShim/Signatures.md @@ -406,9 +406,9 @@ ___ - [`IAsyncEnumerable WhenEach(IEnumerable)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.wheneach#system-threading-tasks-task-wheneach(system-collections-generic-ienumerable((system-threading-tasks-task)))) .NET 9.0 - [`IAsyncEnumerable WhenEach(params Task[])`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.wheneach#system-threading-tasks-task-wheneach(system-threading-tasks-task())) .NET 9.0 - [`Task CompletedTask`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.completedtask) .NET Core 1.0 - - [`Task Delay(TimeSpan, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.delay#system-threading-tasks-task-delay(system-timespan-system-threading-cancellationtoken)) .NET 4.5 - - [`Task Delay(TimeSpan)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.delay#system-threading-tasks-task-delay(system-timespan)) .NET 4.5 - - [`Task FromCanceled(CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.fromcanceled) .NET 4.6 + - [`Task Delay(TimeSpan, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.delay#system-threading-tasks-task-delay(system-timespan-system-threading-cancellationtoken)) .NET Core 1.0 + - [`Task Delay(TimeSpan)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.delay#system-threading-tasks-task-delay(system-timespan)) .NET Core 1.0 + - [`Task FromCanceled(CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.fromcanceled) .NET Core 1.0 - [`Task Run(Action, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.run#system-threading-tasks-task-run(system-action-system-threading-cancellationtoken)) .NET Core 1.0 - [`Task Run(Action)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.run#system-threading-tasks-task-run(system-action)) .NET Core 1.0 - [`Task Run(Func, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.run#system-threading-tasks-task-run(system-func((system-threading-tasks-task))-system-threading-cancellationtoken)) .NET Core 1.0 From eac375610f49326f90a531895d3bbaa4d03b8331 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:59:23 +0000 Subject: [PATCH 10/15] Refactor Task polyfills: move FromCanceled after FromResult, add generic version, limit Delay to pre-NET45, move TimerCallback to NetCore10 Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- PolyShim/NetCore10/Task.cs | 22 ++++++++++++------- .../TimerCallback.cs | 0 PolyShim/Signatures.md | 5 +++-- 3 files changed, 17 insertions(+), 10 deletions(-) rename PolyShim/{NetStandard12 => NetCore10}/TimerCallback.cs (100%) diff --git a/PolyShim/NetCore10/Task.cs b/PolyShim/NetCore10/Task.cs index f0a87c3b..c953be7c 100644 --- a/PolyShim/NetCore10/Task.cs +++ b/PolyShim/NetCore10/Task.cs @@ -151,6 +151,19 @@ public static Task> WhenAny(params Task[] tasks) => WhenAny((IEnumerable>)tasks); #endif + // https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.fromcanceled + public static Task FromCanceled(CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + 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(cancellationToken); + +#if NETFRAMEWORK && !NET45_OR_GREATER // Timer is not available on .NET Standard 1.0 and 1.1 #if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) // https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.delay#system-threading-tasks-task-delay(system-timespan-system-threading-cancellationtoken) @@ -197,14 +210,7 @@ public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) // 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 - - // https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.fromcanceled - public static Task FromCanceled(CancellationToken cancellationToken) - { - var tcs = new TaskCompletionSource(); - tcs.TrySetCanceled(cancellationToken); - return tcs.Task; - } +#endif } } #endif diff --git a/PolyShim/NetStandard12/TimerCallback.cs b/PolyShim/NetCore10/TimerCallback.cs similarity index 100% rename from PolyShim/NetStandard12/TimerCallback.cs rename to PolyShim/NetCore10/TimerCallback.cs diff --git a/PolyShim/Signatures.md b/PolyShim/Signatures.md index 82643630..54568348 100644 --- a/PolyShim/Signatures.md +++ b/PolyShim/Signatures.md @@ -1,8 +1,8 @@ # Signatures -- **Total:** 363 +- **Total:** 364 - **Types:** 78 -- **Members:** 285 +- **Members:** 286 ___ @@ -421,6 +421,7 @@ ___ - [`Task FromResult(T?)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.fromresult) .NET Core 1.0 - [`Task WhenAll(IEnumerable>)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.whenall#system-threading-tasks-task-whenall-1(system-collections-generic-ienumerable((system-threading-tasks-task((-0)))))) .NET Core 1.0 - [`Task WhenAll(params Task[])`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.whenall#system-threading-tasks-task-whenall-1(system-threading-tasks-task((-0))())) .NET Core 1.0 + - [`Task FromCanceled(CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.fromcanceled) .NET Core 1.0 - [`Task Run(Func, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.run#system-threading-tasks-task-run-1(system-func((-0))-system-threading-cancellationtoken)) .NET Core 1.0 - [`Task Run(Func)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.run#system-threading-tasks-task-run-1(system-func((-0)))) .NET Core 1.0 - [`Task Run(Func>, CancellationToken)`](https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.run#system-threading-tasks-task-run-1(system-func((system-threading-tasks-task((-0))))-system-threading-cancellationtoken)) .NET Core 1.0 From 2d535b09eb0e924dee452833408649d1c4dd86e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 07:22:37 +0000 Subject: [PATCH 11/15] Refine polyfills: simplify delay check, move FromCanceled after FromResult, use non-generic TCS, remove redundant disposal Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- PolyShim/Net80/TimeProvider.cs | 2 +- PolyShim/NetCore10/Task.cs | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/PolyShim/Net80/TimeProvider.cs b/PolyShim/Net80/TimeProvider.cs index 9c68a24c..7791be4e 100644 --- a/PolyShim/Net80/TimeProvider.cs +++ b/PolyShim/Net80/TimeProvider.cs @@ -65,7 +65,7 @@ public CancellationTokenSource CreateCancellationTokenSource(TimeSpan delay) { var cts = new CancellationTokenSource(); - if (delay != TimeSpan.Zero && delay.Ticks > 0) + if (delay >= TimeSpan.Zero) cts.CancelAfter(delay); return cts; diff --git a/PolyShim/NetCore10/Task.cs b/PolyShim/NetCore10/Task.cs index c953be7c..79a3f336 100644 --- a/PolyShim/NetCore10/Task.cs +++ b/PolyShim/NetCore10/Task.cs @@ -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 FromCanceled(CancellationToken cancellationToken) + { + var tcs = new TaskCompletionSource(); + 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(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( @@ -151,25 +165,13 @@ public static Task> WhenAny(params Task[] tasks) => WhenAny((IEnumerable>)tasks); #endif - // https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.fromcanceled - public static Task FromCanceled(CancellationToken cancellationToken) - { - var tcs = new TaskCompletionSource(); - 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(cancellationToken); - #if NETFRAMEWORK && !NET45_OR_GREATER // Timer is not available on .NET Standard 1.0 and 1.1 #if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) // 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(); + var tcs = new TaskCompletionSource(); if (cancellationToken.IsCancellationRequested) { @@ -181,8 +183,7 @@ public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) timer = new Timer( _ => { - timer?.Dispose(); - tcs.TrySetResult(true); + tcs.TrySetResult(null); }, null, delay, @@ -191,7 +192,6 @@ public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) var registration = cancellationToken.Register(() => { - timer?.Dispose(); tcs.TrySetCanceled(); }); From 50d462d1a9e9bafe6b0f8b8b9bebc226a1b0c556 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 07:36:32 +0000 Subject: [PATCH 12/15] Make TimeProvider methods virtual for testability, use object type for FromCanceled, simplify Task.Delay, return continuation task Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- PolyShim/Net80/TimeProvider.cs | 4 ++-- PolyShim/NetCore10/Task.cs | 16 ++++------------ 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/PolyShim/Net80/TimeProvider.cs b/PolyShim/Net80/TimeProvider.cs index 7791be4e..3e4a79ad 100644 --- a/PolyShim/Net80/TimeProvider.cs +++ b/PolyShim/Net80/TimeProvider.cs @@ -41,7 +41,7 @@ public TimeSpan GetElapsedTime(long startingTimestamp) => GetElapsedTime(startingTimestamp, GetTimestamp()); #if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER) - public ITimer CreateTimer( + public virtual ITimer CreateTimer( TimerCallback callback, object? state, TimeSpan dueTime, @@ -61,7 +61,7 @@ public virtual Task Delay(TimeSpan delay, CancellationToken cancellationToken = return Task.Delay(delay, cancellationToken); } - public CancellationTokenSource CreateCancellationTokenSource(TimeSpan delay) + public virtual CancellationTokenSource CreateCancellationTokenSource(TimeSpan delay) { var cts = new CancellationTokenSource(); diff --git a/PolyShim/NetCore10/Task.cs b/PolyShim/NetCore10/Task.cs index 79a3f336..736f756d 100644 --- a/PolyShim/NetCore10/Task.cs +++ b/PolyShim/NetCore10/Task.cs @@ -47,7 +47,7 @@ public static Task FromCanceled(CancellationToken cancellationToken) // https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.fromcanceled public static Task FromCanceled(CancellationToken cancellationToken) => - Task.FromCanceled(cancellationToken); + Task.FromCanceled(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) @@ -181,21 +181,15 @@ public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) Timer? timer = null; timer = new Timer( - _ => - { - tcs.TrySetResult(null); - }, + _ => tcs.TrySetResult(null), null, delay, TimeSpan.FromMilliseconds(-1) ); - var registration = cancellationToken.Register(() => - { - tcs.TrySetCanceled(); - }); + var registration = cancellationToken.Register(() => tcs.TrySetCanceled()); - tcs.Task.ContinueWith( + return tcs.Task.ContinueWith( _ => { registration.Dispose(); @@ -203,8 +197,6 @@ public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) }, TaskContinuationOptions.ExecuteSynchronously ); - - return tcs.Task; } // https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.delay#system-threading-tasks-task-delay(system-timespan) From 46832c50a8a2d717f9980941d80ced376bffbb19 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:04:19 +0000 Subject: [PATCH 13/15] Merge timer declaration and assignment in Task.Delay Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- PolyShim/NetCore10/Task.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/PolyShim/NetCore10/Task.cs b/PolyShim/NetCore10/Task.cs index 736f756d..54b0c703 100644 --- a/PolyShim/NetCore10/Task.cs +++ b/PolyShim/NetCore10/Task.cs @@ -179,8 +179,7 @@ public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) return tcs.Task; } - Timer? timer = null; - timer = new Timer( + var timer = new Timer( _ => tcs.TrySetResult(null), null, delay, From 48fe8161f525de8fe932c8345a30a5e1227bf059 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 08:22:59 +0000 Subject: [PATCH 14/15] Remove Delay and CreateCancellationTokenSource methods - not part of TimeProvider API Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- PolyShim/Net80/TimeProvider.cs | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/PolyShim/Net80/TimeProvider.cs b/PolyShim/Net80/TimeProvider.cs index 3e4a79ad..bf4dc54a 100644 --- a/PolyShim/Net80/TimeProvider.cs +++ b/PolyShim/Net80/TimeProvider.cs @@ -49,29 +49,6 @@ TimeSpan period ) => new SystemTimeProviderTimer(dueTime, period, callback, state); #endif -#if FEATURE_TASK - public virtual Task Delay(TimeSpan delay, CancellationToken cancellationToken = default) - { - if (cancellationToken.IsCancellationRequested) - return Task.FromCanceled(cancellationToken); - - if (delay == TimeSpan.Zero) - return Task.CompletedTask; - - return Task.Delay(delay, cancellationToken); - } - - public virtual CancellationTokenSource CreateCancellationTokenSource(TimeSpan delay) - { - var cts = new CancellationTokenSource(); - - if (delay >= TimeSpan.Zero) - cts.CancelAfter(delay); - - return cts; - } -#endif - private sealed class SystemTimeProvider : TimeProvider { public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Local; From 11cb66f74f66f06f09a128bcecc790031e1c53cf Mon Sep 17 00:00:00 2001 From: Oleksii Holub <1935960+Tyrrrz@users.noreply.github.com> Date: Wed, 18 Feb 2026 22:32:36 +0200 Subject: [PATCH 15/15] Update PolyShim/NetCore10/Task.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- PolyShim/NetCore10/Task.cs | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/PolyShim/NetCore10/Task.cs b/PolyShim/NetCore10/Task.cs index 54b0c703..08793022 100644 --- a/PolyShim/NetCore10/Task.cs +++ b/PolyShim/NetCore10/Task.cs @@ -179,23 +179,33 @@ public static Task Delay(TimeSpan delay, CancellationToken cancellationToken) return tcs.Task; } - var timer = new Timer( - _ => tcs.TrySetResult(null), + 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) ); - var registration = cancellationToken.Register(() => tcs.TrySetCanceled()); + registration = cancellationToken.Register(() => CleanupAndSetCanceled()); - return tcs.Task.ContinueWith( - _ => - { - registration.Dispose(); - timer?.Dispose(); - }, - TaskContinuationOptions.ExecuteSynchronously - ); + return tcs.Task; } // https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.delay#system-threading-tasks-task-delay(system-timespan)