Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions PolyShim/Net70/Stopwatch.cs
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
21 changes: 21 additions & 0 deletions PolyShim/Net80/ITimer.cs
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
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exclude from coverage

Copy link
Copy Markdown
Contributor Author

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.

#if FEATURE_ASYNCINTERFACES
, IAsyncDisposable
#endif
{
bool Change(TimeSpan dueTime, TimeSpan period);
}
#endif
95 changes: 95 additions & 0 deletions PolyShim/Net80/TimeProvider.cs
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;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this depend on GetTimestamp?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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
62 changes: 62 additions & 0 deletions PolyShim/NetCore10/Task.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this polyfill not needed after net45

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in eac3756. Task.Delay polyfill now limited to #if NETFRAMEWORK && !NET45_OR_GREATER (only needed before .NET 4.5).

// 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
}
}
Expand Down
12 changes: 12 additions & 0 deletions PolyShim/NetCore10/TimerCallback.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#if NETSTANDARD && !NETSTANDARD1_2_OR_GREATER
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Folder should be named after net core version

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move it to NetCore10

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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
30 changes: 30 additions & 0 deletions PolyShim/NetCore30/Timer.cs
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)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explain in comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f7dfa0d. Added comment on line 1: // Timer is not available on .NET Standard 1.0 and 1.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
24 changes: 21 additions & 3 deletions PolyShim/PolyShim.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Copilot AI Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Microsoft.Bcl.AsyncInterfaces package has been upgraded from version 7.0.0 to 8.0.0 in this PR, but this change is not mentioned in the PR description. While this upgrade appears intentional and consistent with other package updates, it should be documented as it could affect consumers of this library.

Copilot uses AI. Check for mistakes.
Expand Down Expand Up @@ -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 -->
Expand Down
17 changes: 17 additions & 0 deletions PolyShim/PolyShim.targets
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,23 @@
<DefineConstants Condition="$(FeatureValueTuple)"
>$(DefineConstants);FEATURE_VALUETUPLE</DefineConstants
>

<FeatureTimeProvider>false</FeatureTimeProvider>
<FeatureTimeProvider
Condition="@(Reference-&gt;AnyHaveMetadataValue('Identity', 'Microsoft.Bcl.TimeProvider'))"
>true</FeatureTimeProvider
>
<FeatureTimeProvider
Condition="@(PackageDependencies-&gt;AnyHaveMetadataValue('Identity', 'Microsoft.Bcl.TimeProvider'))"
>true</FeatureTimeProvider
>
<FeatureTimeProvider
Condition="$(IsNetCoreApp) AND $([MSBuild]::VersionGreaterThanOrEquals($(TargetFrameworkVersion), '8.0'))"
>true</FeatureTimeProvider
>
<DefineConstants Condition="$(FeatureTimeProvider)"
>$(DefineConstants);FEATURE_TIMEPROVIDER</DefineConstants
>
</PropertyGroup>
</Target>
</Project>
Loading
Loading