Description
This proposal is edited by @tarekgh
Proposal
The aim of this proposal is to introduce time abstraction. This abstraction will include the ability to retrieve the system date and time, either in UTC or local time, as well as timestamps for use in performance or tagging scenarios. Additionally, this abstraction can be used in the Task operations like WaitAsync
and CancellationTokenSource CancelAfter
.
By introducing this new abstraction, it will become possible to replace other existing abstractions that are currently being used in a nonuniform way across various interfaces such as ISystemClock and ITimer. The following are some examples of such interfaces:
- src/Servers/Kestrel/Transport.Quic/src/Internal/ISystemClock.cs
- SignalR/common/Shared/ISystemClock.cs
- Middleware/OutputCaching/src/ISystemClock.cs
- Security/Authentication/Core/src/ISystemClock.cs
- Middleware/ResponseCaching/src/Interfaces/ISystemClock.cs
- Servers/Kestrel/Core/src/Internal/Infrastructure/ISystemClock.cs
- libraries/Microsoft.Extensions.Caching.Abstractions/src/Internal/ISystemClock.cs
- ReverseProxy/Utilities/ITimer.cs
In addition, this new abstraction will enable the creation of tests that can mock time functionality, providing greater flexibility in testing through the ability to customize time operations.
Below are some design notes to consider:
- The proposed abstraction should be compatible with both the .NET and .NET Framework for maximum flexibility.
- It should work on down-level supported versions of both .NET and .NET Core, as it will support netstandard2.0.
- For .NET 8.0, the abstraction will be implemented in the CoreLib.
- For downl-level, the abstraction will be implemented in Microsoft.Extensions.Primitives or any chosen other OOB library which supports ns2.0 and will type forward to core if the library used for .NET 8 or up.
- In .NET 8.0, will add WaitAsync methods to the Task class which work with the time abstraction. Additionally, will provide a new
CancellationTokenSource
constructor that works with abstraction. - In the down-level, we'll provide Task extension methods for WaitAsync operation and creating
CancellationTokenSource
support the abstraction.
APIs proposal
APIs for .NET 8.0 and Down-levels
- Note, the introduced APIs in down-levels will be type forwarded when running on .NET 8.0 and up.
namespace System
{
/// <summary>Provides an abstraction for time. </summary>
public abstract class TimeProvider
{
/// <summary>Initializes the instance. </summary>
protected TimeProvider();
/// <summary>
/// Gets a <see cref="TimeProvider"/> that provides a clock based on <see cref="DateTimeOffset.UtcNow"/>,
/// a time zone based on <see cref="TimeZoneInfo.Local"/>, a high-performance time stamp based on <see cref="Stopwatch"/>,
/// and a timer based on <see cref="Timer"/>.
/// </summary>
public static TimeProvider System { get; }
/// <summary>
/// Creates a <see cref="TimeProvider"/> that provides a clock based on <see cref="DateTimeOffset.UtcNow"/>,
/// a time zone based on <paramref name="timeZone"/>, a high-performance time stamp based on <see cref="Stopwatch"/>,
/// and a timer based on <see cref="Timer"/>.
/// </summary>
public static TimeProvider FromLocalTimeZone(TimeZoneInfo timeZone);
/// <summary>
/// Gets a <see cref="DateTimeOffset"/> value whose date and time are set to the current
/// Coordinated Universal Time (UTC) date and time and whose offset is Zero,
/// all according to this <see cref="TimeProvider"/>'s notion of time.
/// </summary>
public abstract DateTimeOffset UtcNow { get; }
/// <summary>
/// Gets a <see cref="DateTimeOffset"/> value that is set to the current date and time according to this <see cref="TimeProvider"/>'s
/// notion of time based on <see cref="UtcNow"/>, with the offset set to the <see cref="LocalTimeZone"/>'s offset from Coordinated Universal Time (UTC).
/// </summary>
public DateTimeOffset LocalNow { get; }
/// <summary>Gets a <see cref="TimeZoneInfo"/> object that represents the local time zone according to this <see cref="TimeProvider"/>'s notion of time. </summary>
public abstract TimeZoneInfo LocalTimeZone { get; }
/// <summary>Gets the current high-frequency value designed to measure small time intervals with high accuracy in the timer mechanism. </summary>
/// <returns>A long integer representing the high-frequency counter value of the underlying timer mechanism. </returns>
public abstract long GetTimestamp();
/// <summary>Gets the frequency of <see cref="GetTimestamp"/> of high-frequency value per second. </summary>
public abstract long TimestampFrequency { get; }
/// <summary>Gets the elapsed time between two timestamps retrieved using <see cref="GetTimestamp"/>. </summary>
/// <param name="startingTimestamp">The timestamp marking the beginning of the time period. </param>
/// <param name="endingTimestamp">The timestamp marking the end of the time period. </param>
/// <returns>A <see cref="TimeSpan"/> for the elapsed time between the starting and ending timestamps. </returns>
public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp);
/// <summary>Creates a new <see cref="ITimer"/> instance, using <see cref="TimeSpan"/> values to measure time intervals. </summary>
/// <param name="callback">
/// A delegate representing a method to be executed when the timer fires. The method specified for callback should be reentrant,
/// as it may be invoked simultaneously on two threads if the timer fires again before or while a previous callback is still being handled.
/// </param>
/// <param name="state">An object to be passed to the <paramref name="callback"/>. This may be null. </param>
/// <param name="dueTime">The amount of time to delay before <paramref name="callback"/> is invoked. Specify <see cref="Timeout.InfiniteTimeSpan"/> to prevent the timer from starting. Specify <see cref="TimeSpan.Zero"/> to start the timer immediately. </param>
/// <param name="period">The time interval between invocations of <paramref name="callback"/>. Specify <see cref="Timeout.InfiniteTimeSpan"/> to disable periodic signaling. </param>
/// <returns>The newly created <see cref="ITimer"/> instance. </returns>
/// <exception cref="ArgumentNullException"><paramref name="callback"/> is null. </exception>
/// <exception cref="ArgumentOutOfRangeException">The number of milliseconds in the value of <paramref name="dueTime"/> or <paramref name="period"/> is negative and not equal to <see cref="Timeout.Infinite"/>, or is greater than <see cref="int.MaxValue"/>. </exception>
/// <remarks>
/// <para>
/// The delegate specified by the callback parameter is invoked once after <paramref name="dueTime"/> elapses, and thereafter each time the <paramref name="period"/> time interval elapses.
/// </para>
/// <para>
/// If <paramref name="dueTime"/> is zero, the callback is invoked immediately. If <paramref name="dueTime"/> is -1 milliseconds, <paramref name="callback"/> is not invoked; the timer is disabled,
/// but can be re-enabled by calling the <see cref="ITimer.Change"/> method.
/// </para>
/// <para>
/// If <paramref name="period"/> is 0 or -1 milliseconds and <paramref name="dueTime"/> is positive, <paramref name="callback"/> is invoked once; the periodic behavior of the timer is disabled,
/// but can be re-enabled using the <see cref="ITimer.Change"/> method.
/// </para>
/// <para>
/// The return <see cref="ITimer"/> instance will be implicitly rooted while the timer is still scheduled.
/// </para>
/// <para>
/// <see cref="CreateTimer"/> captures the <see cref="ExecutionContext"/> and stores that with the <see cref="ITimer"/> for use in invoking <paramref name="callback"/>
/// each time it's called. That capture can be suppressed with <see cref="ExecutionContext.SuppressFlow"/>.
/// </para>
/// </remarks>
public abstract ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period);
}
}
namespace System.Threading
{
/// <summary>Represents a timer that can have its due time and period changed. </summary>
/// <remarks>
/// Implementations of <see cref="Change"/>, <see cref="IDisposable.Dispose"/>, and <see cref="IAsyncDisposable.DisposeAsync"/>
/// must all be thread-safe such that the timer instance may be accessed concurrently from multiple threads.
/// </remarks>
public interface ITimer : IDisposable, IAsyncDisposable
{
/// <summary>Changes the start time and the interval between method invocations for a timer, using <see cref="TimeSpan"/> values to measure time intervals. </summary>
/// <param name="dueTime">
/// A <see cref="TimeSpan"/> representing the amount of time to delay before invoking the callback method specified when the <see cref="ITimer"/> was constructed.
/// Specify <see cref="Timeout.InfiniteTimeSpan"/> to prevent the timer from restarting. Specify <see cref="TimeSpan.Zero"/> to restart the timer immediately.
/// </param>
/// <param name="period">
/// The time interval between invocations of the callback method specified when the Timer was constructed.
/// Specify <see cref="Timeout.InfiniteTimeSpan"/> to disable periodic signaling.
/// </param>
/// <returns><see langword="true"/> if the timer was successfully updated; otherwise, <see langword="false"/>. </returns>
/// <exception cref="ObjectDisposedException">The timer has already been disposed. </exception>
/// <exception cref="ArgumentOutOfRangeException">The <paramref name="dueTime"/> or <paramref name="period"/> parameter, in milliseconds, is less than -1 or greater than 4294967294. </exception>
bool Change(TimeSpan dueTime, TimeSpan period);
}
APIs for .NET 8.0 Only
namespace System.Threading
{
- public sealed class Timer : MarshalByRefObject, IDisposable, IAsyncDisposable
+ public sealed class Timer : MarshalByRefObject, IDisposable, IAsyncDisposable, ITimer
{
}
public class CancellationTokenSource : IDisposable
{
+ /// <summary>Initializes a new instance of the <see cref="CancellationTokenSource"/> class that will be canceled after the specified <see cref="TimeSpan"/>. </summary>
+ /// <param name="delay">The time interval to wait before canceling this <see cref="CancellationTokenSource"/>. </param>
+ /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret the <paramref name="delay"/>. </param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="delay"/>'s <see cref="TimeSpan.TotalMilliseconds"/> is less than -1 or greater than <see cref="uint.MaxValue"/> - 1. </exception>
+ /// <exception cref="ArgumentNullException"><paramref name="timeProvider"/> is null. </exception>
+ /// <remarks>
+ /// The countdown for the delay starts during the call to the constructor. When the delay expires,
+ /// the constructed <see cref="CancellationTokenSource"/> is canceled, if it has
+ /// not been canceled already. Subsequent calls to CancelAfter will reset the delay for the constructed
+ /// <see cref="CancellationTokenSource"/>, if it has not been canceled already.
+ /// </remarks>
+ public CancellationTokenSource(TimeSpan delay, TimeProvider timeProvider);
}
public sealed class PeriodicTimer : IDisposable
{
+ /// <summary>Initializes the timer. </summary>
+ /// <param name="period">The time interval between returning the next enumerated value.</param>
+ /// <param name="timeProvider">The <see cref="TimeProvider"/> used to interpret <paramref name="period"/>. </param>
+ /// <exception cref="ArgumentOutOfRangeException"><paramref name="period"/> must be <see cref="Timeout.InfiniteTimeSpan"/> or represent a number of milliseconds equal to or larger than 1 and smaller than <see cref="uint.MaxValue"/>. </exception>
+ /// <exception cref="ArgumentNullException"><paramref name="timeProvider"/> is null</exception>
+ public PeriodicTimer(TimeSpan period, TimeProvider timeProvider);
}
}
namespace System.Threading.Tasks
{
public class Task<TResult> : Task
{
+ /// <summary>Gets a <see cref="Task{TResult}"/> that will complete when this <see cref="Task{TResult}"/> completes or when the specified timeout expires. </summary>
+ /// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed. </param>
+ /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="timeout"/>. </param>
+ /// <returns>The <see cref="Task{TResult}"/> representing the asynchronous wait. It may or may not be the same instance as the current instance. </returns>
+ public new Task<TResult> WaitAsync(TimeSpan timeout, TimeProvider timeProvider);
+ /// <summary>Gets a <see cref="Task{TResult}"/> that will complete when this <see cref="Task{TResult}"/> completes, when the specified timeout expires, or when the specified <see cref="CancellationToken"/> has cancellation requested. </summary>
+ /// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed. </param>
+ /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="timeout"/>. </param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for a cancellation request. </param>
+ /// <returns>The <see cref="Task{TResult}"/> representing the asynchronous wait. It may or may not be the same instance as the current instance. </returns>
+ public new Task<TResult> WaitAsync(TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken);
}
public class Task : IAsyncResult, IDisposable
{
+ /// <summary>Gets a <see cref="Task"/> that will complete when this <see cref="Task"/> completes or when the specified timeout expires. </summary>
+ /// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed. </param>
+ /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="timeout"/>. </param>
+ /// <returns>The <see cref="Task"/> representing the asynchronous wait. It may or may not be the same instance as the current instance. </returns>
+ public Task WaitAsync(TimeSpan timeout, TimeProvider timeProvider);
+ /// <summary>Gets a <see cref="Task"/> that will complete when this <see cref="Task"/> completes, when the specified timeout expires, or when the specified <see cref="CancellationToken"/> has cancellation requested. </summary>
+ /// <param name="timeout">The timeout after which the <see cref="Task"/> should be faulted with a <see cref="TimeoutException"/> if it hasn't otherwise completed. </param>
+ /// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret <paramref name="timeout"/>. </param>
+ /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for a cancellation request. </param>
+ /// <returns>The <see cref="Task"/> representing the asynchronous wait. It may or may not be the same instance as the current instance. </returns>
+ public Task WaitAsync(TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken);
}
}
APIs for down-level Only
namespace System.Threading.Tasks
{
public static class TimeProviderTaskExtensions
{
public static async Task<TResult> WaitAsync<TResult>(this Task<TResult> task, TimeSpan timeout, TimeProvider timeProvider);
public static async Task<TResult> WaitAsync<TResult>(this Task<TResult> task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken);
public static async Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider);
public static async Task WaitAsync(this Task task, TimeSpan timeout, TimeProvider timeProvider, CancellationToken cancellationToken);
}
Possible APIs addition for .NET 8.0 and down-level
namespace System.Threading.Tasks
{
public static class TimeProviderTaskExtensions
{
/// <summary>Initializes a new instance of the <see cref="CancellationTokenSource"/> class that will be canceled after the specified <see cref="TimeSpan"/>. </summary>
/// <param name="timeProvider">The <see cref="TimeProvider"/> with which to interpret the <paramref name="delay"/>. </param>
/// <param name="delay">The time interval to wait before canceling this <see cref="CancellationTokenSource"/>. </param>
/// <exception cref="ArgumentOutOfRangeException"><paramref name="delay"/>'s <see cref="TimeSpan.TotalMilliseconds"/> is less than -1 or greater than <see cref="uint.MaxValue"/> - 1. </exception>
/// <remarks>
/// The countdown for the delay starts during the call to the constructor. When the delay expires,
/// the constructed <see cref="CancellationTokenSource"/> is canceled, if it has
/// not been canceled already. Subsequent calls to CancelAfter will reset the delay for the constructed
/// <see cref="CancellationTokenSource"/>, if it has not been canceled already.
/// </remarks>
public static CancellationTokenSource CreateCancellationTokenSource(this TimeProvider timeProvider, TimeSpan delay) ;
}
}
End of the @tarekgh edit
The Original Proposal
Motivation
The ISystemClock
interface exists in:
Microsoft.AspNetCore.Authentication
Microsoft.AspNetCore.ResponseCaching
Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
Microsoft.Extensions.Internal
There is a small difference exists between implementations, but in most cases it can be moved out. In case of Kestrel's clock which has a specific logic inside, there could be made a new interface IScopedSystemClock
which will provide the scope start time as UtcNow
does now. Therefore, looks like all of them could be merged into a single class/interface and put into Microsoft.Extensions.Primitives
.
The same interface often implemented by developers themselves to be used by microservices and applications utilizing dependency injection.
Having a common implementation of the data provider pattern will free users from repeating the same simple code many times and will allow to test apps in conjunction with ASP.NET internals without changing an environment.
Proposed API
The ISystemClock
defines a way to get the current time in UTC timezone, and it has a simple implementation:
public interface ISystemClock
{
DateTimeOffset UtcNow { get; }
}
public class SystemClock : ISystemClock
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}
Originally proposed in dotnet/aspnetcore#16844.