¿Qué es TimeProvider?
System.TimeProvider es una abstracción de tiempo que proporciona un momento dado como un tipo de DateTimeOffset. Al usar TimeProvider
, aseguras que tu código sea comprobable y predecible. TimeProvider
se introdujo en .NET 8 y también está disponible para .NET Framework 4.7+ y .NET Standard 2.0 como paquete NuGet.
La clase TimeProvider define las siguientes funcionalidades:
- Proporciona acceso a la fecha y hora a través de TimeProvider.GetUtcNow() y TimeProvider.GetLocalNow().
- Marcas de tiempo de alta frecuencia con TimeProvider.GetTimestamp().
- Mida el tiempo entre dos marcas de tiempo con TimeProvider.GetElapsedTime.
- Temporizadores de alta resolución con TimeProvider.CreateTimer(TimerCallback, Object, TimeSpan, TimeSpan).
- Obtenga la zona horaria actual con TimeProvider.LocalTimeZone.
Implementación predeterminada
.NET proporciona una implementación de TimeProvider a través de la propiedad TimeProvider.System, con las siguientes características:
- Fecha y hora se calculan con DateTimeOffset.UtcNow y TimeZoneInfo.Local.
- Las marcas de tiempo se proporcionan mediante System.Diagnostics.Stopwatch.
- Los temporizadores se implementan a través de una clase interna y se exponen como System.Threading.ITimer.
En el ejemplo siguiente se muestra el uso de TimeProvider para obtener la fecha y hora actuales:
Console.WriteLine($"Local: {TimeProvider.System.GetLocalNow()}");
Console.WriteLine($"Utc: {TimeProvider.System.GetUtcNow()}");
/* This example produces output similar to the following:
*
* Local: 12/5/2024 10:41:14 AM -08:00
* Utc: 12/5/2024 6:41:14 PM +00:00
*/
Console.WriteLine($"Local: {TimeProvider.System.GetLocalNow()}")
Console.WriteLine($"Utc: {TimeProvider.System.GetUtcNow()}")
' This example produces output similar to the following
'
' Local: 12/5/2024 10:41:14 AM -08:00
' Utc: 12/5/2024 6:41:14 PM +00:00
En el ejemplo siguiente se muestra la captura del tiempo transcurrido con TimeProvider.GetTimestamp():
long stampStart = TimeProvider.System.GetTimestamp();
Console.WriteLine($"Starting timestamp: {stampStart}");
long stampEnd = TimeProvider.System.GetTimestamp();
Console.WriteLine($"Ending timestamp: {stampEnd}");
Console.WriteLine($"Elapsed time: {TimeProvider.System.GetElapsedTime(stampStart, stampEnd)}");
Console.WriteLine($"Nanoseconds: {TimeProvider.System.GetElapsedTime(stampStart, stampEnd).TotalNanoseconds}");
/* This example produces output similar to the following:
*
* Starting timestamp: 55185546133
* Ending timestamp: 55185549929
* Elapsed time: 00:00:00.0003796
* Nanoseconds: 379600
*/
Dim stampStart As Long = TimeProvider.System.GetTimestamp()
Console.WriteLine($"Starting timestamp: {stampStart}")
Dim stampEnd As Long = TimeProvider.System.GetTimestamp()
Console.WriteLine($"Ending timestamp: {stampEnd}")
Console.WriteLine($"Elapsed time: {TimeProvider.System.GetElapsedTime(stampStart, stampEnd)}")
Console.WriteLine($"Nanoseconds: {TimeProvider.System.GetElapsedTime(stampStart, stampEnd).TotalNanoseconds}")
' This example produces output similar to the following:
'
' Starting timestamp: 55185546133
' Ending timestamp: 55185549929
' Elapsed time: 00:00:00.0003796
' Nanoseconds: 379600
Implementación de FakeTimeProvider
El paquete NuGet Microsoft.Extensions.TimeProvider.Testing proporciona una implementación de TimeProvider
controlable diseñada para pruebas unitarias.
En la lista siguiente se describen algunas de las funcionalidades de la clase FakeTimeProvider:
- Establezca una fecha y hora específicas.
- Avance automáticamente la fecha y hora por una cantidad especificada cada vez que se lea la fecha y hora.
- Avance manualmente la fecha y la hora.
Implementación personalizada
Si bien FakeTimeProvider debe abarcar la mayoría de los escenarios que requieren predictibilidad con el tiempo, puede seguir implementando su propia versión. Cree una nueva clase procedente de TimeProvider e invalide miembros para controlar cómo se proporciona el tiempo. Por ejemplo, la siguiente clase solo proporciona una fecha única, la fecha del aterrizaje lunar:
public class MoonLandingTimeProviderPST: TimeProvider
{
// July 20, 1969, at 20:17:40 UTC
private readonly DateTimeOffset _specificDateTime = new(1969, 7, 20, 20, 17, 40, TimeZoneInfo.Utc.BaseUtcOffset);
public override DateTimeOffset GetUtcNow() => _specificDateTime;
public override TimeZoneInfo LocalTimeZone => TimeZoneInfo.FindSystemTimeZoneById("PST");
}
Public Class MoonLandingTimeProviderPST
Inherits TimeProvider
'July 20, 1969, at 20:17:40 UTC
Private ReadOnly _specificDateTime As New DateTimeOffset(1969, 7, 20, 20, 17, 40, TimeZoneInfo.Utc.BaseUtcOffset)
Public Overrides Function GetUtcNow() As DateTimeOffset
Return _specificDateTime
End Function
Public Overrides ReadOnly Property LocalTimeZone As TimeZoneInfo
Get
Return TimeZoneInfo.FindSystemTimeZoneById("PST")
End Get
End Property
End Class
Si el código que usa esta clase llama a MoonLandingTimeProviderPST.GetUtcNow
, se devuelve la fecha del aterrizaje lunar en UTC. Si se llama a MoonLandingTimeProviderPST.GetLocalNow
, la clase base aplica MoonLandingTimeProviderPST.LocalTimeZone
a GetUtcNow
y devuelve la fecha y hora de aterrizaje de la luna en la zona horaria PST.
Para demostrar la utilidad de controlar el tiempo, considere el ejemplo siguiente. Supongamos que está escribiendo una aplicación de calendario que envía un saludo al usuario cuando la aplicación se abre por primera vez cada día. La aplicación dice un saludo especial cuando el día actual tiene un evento asociado, como el aniversario del aterrizaje lunar.
public static class CalendarHelper
{
static readonly DateTimeOffset MoonLandingDateTime = new(1969, 7, 20, 20, 17, 40, TimeZoneInfo.Utc.BaseUtcOffset);
public static void SendGreeting(TimeProvider currentTime, string name)
{
DateTimeOffset localTime = currentTime.GetLocalNow();
Console.WriteLine($"Good morning, {name}!");
Console.WriteLine($"The date is {localTime.Date:d} and the day is {localTime.Date.DayOfWeek}.");
if (localTime.Date.Month == MoonLandingDateTime.Date.Month
&& localTime.Date.Day == MoonLandingDateTime.Date.Day)
{
Console.WriteLine("Did you know that on this day in 1969 humans landed on the Moon?");
}
Console.WriteLine($"I hope you enjoy your day!");
}
}
Public Module CalendarHelper
ReadOnly MoonLandingDateTime As DateTimeOffset = #7/20/1969 20:17:40#
Public Sub SendGreeting(currentTime As TimeProvider, name As String)
Dim localTime As DateTimeOffset = currentTime.GetLocalNow()
Console.WriteLine($"Good morning, {name}!")
Console.WriteLine($"The date is {localTime.Date:d} and the day is {localTime.Date.DayOfWeek}.")
If (localTime.Date.Month = MoonLandingDateTime.Date.Month _
And localTime.Date.Day = MoonLandingDateTime.Date.Day) Then
Console.WriteLine("Did you know that on this day in 1969 humans landed on the Moon?")
End If
Console.WriteLine($"I hope you enjoy your day!")
End Sub
End Module
Es posible que esté inclinado a escribir el código anterior con DateTime o DateTimeOffset para obtener la fecha y hora actuales, en lugar de TimeProvider. Pero con las pruebas unitarias, es difícil solucionar DateTime o DateTimeOffset directamente. Tendría que ejecutar las pruebas en el día y mes del aterrizaje lunar o bien abstraer aún más el código en unidades más pequeñas pero comprobables.
El funcionamiento normal de la aplicación usa TimeProvider.System
para recuperar la fecha y hora actuales:
CalendarHelper.SendGreeting(TimeProvider.System, "Eric Solomon");
/* This example produces output similar to the following:
*
* Good morning, Eric Solomon!
* The date is 12/5/2024 and the day is Thursday.
* I hope you enjoy your day!
*/
CalendarHelper.SendGreeting(TimeProvider.System, "Eric Solomon")
' This example produces output similar to the following:
'
' Good morning, Eric Solomon!
' The date is 12/5/2024 and the day is Thursday.
' I hope you enjoy your day!
Y las pruebas unitarias se pueden escribir para probar escenarios específicos, como probar el aniversario del aterrizaje lunar:
CalendarHelper.SendGreeting(new MoonLandingTimeProviderPST(), "Eric Solomon");
/* This example produces output similar to the following:
*
* Good morning, Eric Solomon!
* The date is 7/20/1969 and the day is Sunday.
* Did you know that on this day in 1969 humans landed on the Moon?
* I hope you enjoy your day!
*/
CalendarHelper.SendGreeting(New MoonLandingTimeProviderPST(), "Eric Solomon")
' This example produces output similar to the following:
'
' Good morning, Eric Solomon!
' The date is 7/20/1969 and the day is Sunday.
' Did you know that on this day in 1969 humans landed on the Moon?
' I hope you enjoy your day!
Uso con .NET
A partir de .NET 8, la biblioteca en tiempo de ejecución proporciona la clase TimeProvider. Las versiones anteriores de .NET o bibliotecas destinadas a .NET Standard 2.0 deben hacer referencia al paquete NuGet Microsoft.Bcl.TimeProvider .
Los métodos siguientes relacionados con la programación asincrónica funcionan con TimeProvider
:
- CancellationTokenSource(TimeSpan, TimeProvider)
- Task.Delay(TimeSpan, TimeProvider)
- Task.Delay(TimeSpan, TimeProvider, CancellationToken)
- Task.WaitAsync(TimeSpan, TimeProvider)
- Task.WaitAsync(TimeSpan, TimeProvider, CancellationToken)
Uso con .NET Framework
TimeProvider se implementa con el paquete NuGet Microsoft.Bcl.TimeProvider.
Se ha agregado compatibilidad para trabajar con TimeProvider
en escenarios de programación asincrónica mediante los siguientes métodos de extensión:
- TimeProviderTaskExtensions.CreateCancellationTokenSource(TimeProvider, TimeSpan)
- TimeProviderTaskExtensions.Delay(TimeProvider, TimeSpan, CancellationToken)
- TimeProviderTaskExtensions.WaitAsync(Task, TimeSpan, TimeProvider, CancellationToken)
- TimeProviderTaskExtensions.WaitAsync<TResult>(Task<TResult>, TimeSpan, TimeProvider, CancellationToken)