Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
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
159 changes: 159 additions & 0 deletions PolyShim/Net80/TimeProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#if !FEATURE_TIMEPROVIDER
#nullable enable
// ReSharper disable RedundantUsingDirective
// ReSharper disable CheckNamespace
// ReSharper disable InconsistentNaming
// ReSharper disable PartialTypeWithSinglePart

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using SystemThreading = System.Threading;

namespace System;

// https://learn.microsoft.com/dotnet/api/system.timeprovider
[ExcludeFromCodeCoverage]
internal abstract class TimeProvider
{
public static TimeProvider System { get; } = new SystemTimeProvider();

protected TimeProvider() { }

public virtual DateTimeOffset GetUtcNow() => DateTimeOffset.UtcNow;

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 abstract TimeZoneInfo LocalTimeZone { get; }

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.

Properties should be above methods

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 be2f69e. Moved LocalTimeZone property above GetUtcNow() method.


public virtual long GetTimestamp() => Stopwatch.GetTimestamp();

public TimeSpan GetElapsedTime(long startingTimestamp) =>

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 below the other overload

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. Moved GetElapsedTime(long startingTimestamp) below the two-parameter overload GetElapsedTime(long startingTimestamp, long endingTimestamp).

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

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.

Or we can polyfill that?

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.

Added in be2f69e. Created Net70/Stopwatch.cs polyfill with GetElapsedTime(long startingTimestamp, long endingTimestamp) and GetElapsedTime(long startingTimestamp) extension methods. TimeProvider now delegates to the polyfilled method.

var tickFrequency = Stopwatch.Frequency;
var ticks = endingTimestamp - startingTimestamp;

if (tickFrequency == TimeSpan.TicksPerSecond)
{
return new TimeSpan(ticks);
}
else if (tickFrequency > TimeSpan.TicksPerSecond)
{
var ticksPerStopwatchTick = (double)tickFrequency / TimeSpan.TicksPerSecond;
return new TimeSpan((long)(ticks / ticksPerStopwatchTick));
}
else
{
var ticksPerStopwatchTick = (double)TimeSpan.TicksPerSecond / tickFrequency;
return new TimeSpan((long)(ticks * ticksPerStopwatchTick));
}
}

// Timer and TimerCallback are not available on .NET Standard 1.0 and 1.1

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.

ITimer is polyfilled. Can we polyfill TimerCallback?

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.

Added in be2f69e. Created NetStandard12/TimerCallback.cs polyfill defining the delegate for .NET Standard < 1.2. CreateTimer is now available on all frameworks except .NET Standard 1.0/1.1.

#if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER)
public SystemThreading.ITimer CreateTimer(
SystemThreading.TimerCallback callback,
object? state,
TimeSpan dueTime,
TimeSpan period
) => new SystemTimeProviderTimer(dueTime, period, callback, state);
#endif

// Task.Delay, Timeout.InfiniteTimeSpan, and CancellationTokenSource(TimeSpan) require .NET 4.5+

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.

Polyfill the missing members so they're available everywhere where Task is available (except infinite timeout, that should be available everywhere).

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.

Added in be2f69e. Created Net45/Task.cs with Task.Delay polyfill for frameworks < .NET 4.5. Removed all nested conditionals - Delay and CreateCancellationTokenSource are now available wherever FEATURE_TASK is defined. Note: CancellationTokenSource(TimeSpan) constructor doesn't exist on .NET 4.0, so CreateCancellationTokenSource returns a plain instance for those frameworks.

#if FEATURE_TASK && (NETSTANDARD1_3_OR_GREATER || NETCOREAPP || NET45_OR_GREATER)
public virtual Task Delay(
TimeSpan delay,
SystemThreading.CancellationToken cancellationToken = default
)
{
if (delay < TimeSpan.Zero && delay != SystemThreading.Timeout.InfiniteTimeSpan)
throw new ArgumentOutOfRangeException(nameof(delay));

if (cancellationToken.IsCancellationRequested)
{
var tcs = new TaskCompletionSource<bool>();
tcs.SetCanceled();
return tcs.Task;

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.

Use Task.FromCancellation or whatever. If unavailable, add Polyfill

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. Created Net46/Task.cs polyfill with Task.FromCanceled(CancellationToken) and updated TimeProvider to use it.

}

if (delay == TimeSpan.Zero)
return Task.CompletedTask;

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.

We have this polyfilled for all TFMs

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 a4b3590. Now using the polyfilled Task.CompletedTask directly without conditional compilation.


return Task.Delay(delay, cancellationToken);
}

public SystemThreading.CancellationTokenSource CreateCancellationTokenSource(TimeSpan delay)

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.

Do we need a fully qualified return type?

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 be2f69e. Changed from SystemThreading.CancellationTokenSource to just CancellationTokenSource by updating using statements.

{
if (delay < TimeSpan.Zero && delay != SystemThreading.Timeout.InfiniteTimeSpan)

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.

Drop the guards in all polyfills

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 be2f69e. Removed all ArgumentOutOfRangeException validation checks from Delay() and CreateCancellationTokenSource().

throw new ArgumentOutOfRangeException(nameof(delay));

if (delay == SystemThreading.Timeout.InfiniteTimeSpan)
return new SystemThreading.CancellationTokenSource();

return new SystemThreading.CancellationTokenSource(delay);
}
#endif

private sealed class SystemTimeProvider : TimeProvider
{
public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.Local;
}

// Timer and TimerCallback are not available on .NET Standard 1.0 and 1.1
#if !(NETSTANDARD && !NETSTANDARD1_2_OR_GREATER)
private sealed class SystemTimeProviderTimer : SystemThreading.ITimer
{
private readonly SystemThreading.Timer _timer;

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.

Can this be polyfilled?

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.

Timer itself is part of the BCL and can't be polyfilled as it's a native type. However, the CreateTimer method now works on all frameworks where Timer is available (everywhere except .NET Standard 1.0/1.1) thanks to the TimerCallback polyfill.


public SystemTimeProviderTimer(
TimeSpan dueTime,
TimeSpan period,
SystemThreading.TimerCallback callback,
object? state
)
{
_timer = new SystemThreading.Timer(callback, state, dueTime, period);
}

public bool Change(TimeSpan dueTime, TimeSpan period)
{
try
{
return _timer.Change(dueTime, period);
}
catch
{
return false;
}
}

public void Dispose()
{
_timer.Dispose();
}

#if FEATURE_ASYNCINTERFACES
public ValueTask DisposeAsync()
{
#if NETCOREAPP3_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER
return _timer.DisposeAsync();

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.

Just polyfill this method on Timer, same as we do for Stream

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.

Added in be2f69e. Created NetCore30/Timer.cs with DisposeAsync() extension method. SystemTimeProviderTimer now calls _timer.DisposeAsync() directly, which uses the polyfill on older frameworks.

#else
_timer.Dispose();
return default;

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.

Just try cast to IAsyncDisposable

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.

The try-cast approach doesn't work because Timer doesn't implement IAsyncDisposable on net461 and similar frameworks - the compiler rejects the cast at compile time. Kept the conditional compilation in a4b3590 to match the actual Timer API availability.

#endif
}
#endif
}
#endif
}
#endif
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

Copilot AI Feb 18, 2026

Copy link

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>
8 changes: 6 additions & 2 deletions PolyShim/Signatures.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Signatures

- **Total:** 355
- **Types:** 76
- **Total:** 357
- **Types:** 78
- **Members:** 279

___
Expand Down Expand Up @@ -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)) <sup><sub>.NET Core 2.0</sub></sup>
- `IsExternalInit`
- [**[class]**](https://learn.microsoft.com/dotnet/api/system.runtime.compilerservices.isexternalinit) <sup><sub>.NET 5.0</sub></sup>
- `ITimer`
- [**[interface]**](https://learn.microsoft.com/dotnet/api/system.threading.itimer) <sup><sub>.NET 8.0</sub></sup>
- `KeyValuePair<TKey, TValue>`
- [`void Deconstruct(out TKey, out TValue)`](https://learn.microsoft.com/dotnet/api/system.collections.generic.keyvaluepair-2.deconstruct) <sup><sub>.NET Core 2.0</sub></sup>
- `LibraryImportAttribute`
Expand Down Expand Up @@ -441,6 +443,8 @@ ___
- [`void Write(ReadOnlySpan<char>)`](https://learn.microsoft.com/dotnet/api/system.io.textwriter.write#system-io-textwriter-write(system-readonlyspan((system-char)))) <sup><sub>.NET Core 2.1</sub></sup>
- `ThreadAbortException`
- [**[class]**](https://learn.microsoft.com/dotnet/api/system.threading.threadabortexception) <sup><sub>.NET Core 2.0</sub></sup>
- `TimeProvider`
- [**[class]**](https://learn.microsoft.com/dotnet/api/system.timeprovider) <sup><sub>.NET 8.0</sub></sup>
- `TimeSpan`
- [`TimeSpan FromMilliseconds(long, long)`](https://learn.microsoft.com/dotnet/api/system.timespan.frommilliseconds#system-timespan-frommilliseconds(system-int64-system-int64)) <sup><sub>.NET 9.0</sub></sup>
- [`TimeSpan FromMilliseconds(long)`](https://learn.microsoft.com/dotnet/api/system.timespan.frommilliseconds#system-timespan-frommilliseconds(system-int64)) <sup><sub>.NET 10.0</sub></sup>
Expand Down
1 change: 1 addition & 0 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ Currently, **PolyShim** recognizes the following packages:
- [`Microsoft.Bcl.AsyncInterfaces`](https://nuget.org/packages/Microsoft.Bcl.AsyncInterfaces) — `IAsyncEnumerable<T>`, `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<T>`, such as `Task.WhenEach(...)`:
Expand Down