diff --git a/PolyShim/Net80/TimeProvider.cs b/PolyShim/Net80/TimeProvider.cs
index bf4dc54..ab01b0c 100644
--- a/PolyShim/Net80/TimeProvider.cs
+++ b/PolyShim/Net80/TimeProvider.cs
@@ -40,21 +40,18 @@ public virtual TimeSpan GetElapsedTime(long startingTimestamp, long endingTimest
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;
@@ -90,6 +87,5 @@ public void Dispose()
public ValueTask DisposeAsync() => _timer.DisposeAsync();
#endif
}
-#endif
}
#endif
diff --git a/PolyShim/NetCore10/Timer.cs b/PolyShim/NetCore10/Timer.cs
new file mode 100644
index 0000000..d1a5b3e
--- /dev/null
+++ b/PolyShim/NetCore10/Timer.cs
@@ -0,0 +1,156 @@
+#if NETSTANDARD && !NETSTANDARD1_2_OR_GREATER
+#nullable enable
+// ReSharper disable RedundantUsingDirective
+// ReSharper disable CheckNamespace
+// ReSharper disable InconsistentNaming
+// ReSharper disable PartialTypeWithSinglePart
+
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+
+namespace System.Threading;
+
+// https://learn.microsoft.com/dotnet/api/system.threading.timer
+[ExcludeFromCodeCoverage]
+internal sealed class Timer(TimerCallback callback, object? state) : IDisposable
+{
+ private CancellationTokenSource? _cts;
+ private volatile bool _disposed;
+
+ public Timer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)
+ : this(callback ?? throw new ArgumentNullException(nameof(callback)), state)
+ {
+ Schedule(dueTime, period);
+ }
+
+ public Timer(TimerCallback callback, object? state, int dueTime, int period)
+ : this(
+ callback,
+ state,
+ TimeSpan.FromMilliseconds(dueTime),
+ TimeSpan.FromMilliseconds(period)
+ ) { }
+
+ public Timer(TimerCallback callback, object? state, long dueTime, long period)
+ : this(
+ callback,
+ state,
+ TimeSpan.FromMilliseconds(dueTime),
+ TimeSpan.FromMilliseconds(period)
+ ) { }
+
+ public Timer(TimerCallback callback)
+ : this(callback, null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan) { }
+
+ private static void Start(
+ TimerCallback callback,
+ object? state,
+ TimeSpan dueTime,
+ TimeSpan period,
+ CancellationToken cancellationToken
+ )
+ {
+ if (dueTime == Timeout.InfiniteTimeSpan)
+ return;
+
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ if (dueTime > TimeSpan.Zero)
+ await Task.Delay(dueTime, cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+
+ if (cancellationToken.IsCancellationRequested)
+ return;
+
+ callback(state);
+
+ if (period == Timeout.InfiniteTimeSpan || period == TimeSpan.Zero)
+ return;
+
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ try
+ {
+ await Task.Delay(period, cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+
+ if (cancellationToken.IsCancellationRequested)
+ return;
+
+ callback(state);
+ }
+ });
+ }
+
+ private void Schedule(TimeSpan dueTime, TimeSpan period)
+ {
+ if (_disposed)
+ throw new ObjectDisposedException(nameof(Timer));
+
+ if (dueTime != Timeout.InfiniteTimeSpan && dueTime < TimeSpan.Zero)
+ throw new ArgumentOutOfRangeException(nameof(dueTime));
+ if (period != Timeout.InfiniteTimeSpan && period < TimeSpan.Zero)
+ throw new ArgumentOutOfRangeException(nameof(period));
+
+ var cts = new CancellationTokenSource();
+ var token = cts.Token;
+ var oldCts = Interlocked.Exchange(ref _cts, cts);
+
+ try
+ {
+ oldCts?.Cancel();
+ }
+ catch (ObjectDisposedException) { }
+
+ oldCts?.Dispose();
+
+ Start(callback, state, dueTime, period, token);
+
+ // Handle race where Dispose completes after the initial _disposed check
+ // but before/just after the exchange: ensure the newly created CTS
+ // is also cancelled and disposed so it doesn't leak or keep firing.
+ if (_disposed)
+ {
+ try
+ {
+ cts.Cancel();
+ }
+ catch (ObjectDisposedException) { }
+
+ cts.Dispose();
+ }
+ }
+
+ public bool Change(TimeSpan dueTime, TimeSpan period)
+ {
+ Schedule(dueTime, period);
+ return true;
+ }
+
+ public bool Change(int dueTime, int period) =>
+ Change(TimeSpan.FromMilliseconds(dueTime), TimeSpan.FromMilliseconds(period));
+
+ public bool Change(long dueTime, long period) =>
+ Change(TimeSpan.FromMilliseconds(dueTime), TimeSpan.FromMilliseconds(period));
+
+ public void Dispose()
+ {
+ if (_disposed)
+ return;
+
+ _disposed = true;
+ _cts?.Cancel();
+ _cts?.Dispose();
+ }
+}
+#endif
diff --git a/PolyShim/NetCore30/Timer.cs b/PolyShim/NetCore30/Timer.cs
index b2ae961..1afc8b5 100644
--- a/PolyShim/NetCore30/Timer.cs
+++ b/PolyShim/NetCore30/Timer.cs
@@ -1,6 +1,4 @@
#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
// ReSharper disable CheckNamespace
@@ -27,4 +25,3 @@ public ValueTask DisposeAsync()
}
}
#endif
-#endif
diff --git a/PolyShim/Signatures.md b/PolyShim/Signatures.md
index 8a19aca..0b398ad 100644
--- a/PolyShim/Signatures.md
+++ b/PolyShim/Signatures.md
@@ -1,7 +1,7 @@
# Signatures
-- **Total:** 372
-- **Types:** 78
+- **Total:** 373
+- **Types:** 79
- **Members:** 294
___
@@ -462,6 +462,7 @@ ___
- `TimeProvider`
- [**[class]**](https://learn.microsoft.com/dotnet/api/system.timeprovider) .NET 8.0
- `Timer`
+ - [**[class]**](https://learn.microsoft.com/dotnet/api/system.threading.timer) .NET Core 1.0
- [`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