Che cos'è TimeProvider?
System.TimeProvider è un'astrazione del tempo che fornisce un punto nel tempo come tipo di DateTimeOffset. Utilizzando TimeProvider
, ci si assicura che il codice sia testabile e prevedibile.
TimeProvider
è stato introdotto in .NET 8 ed è disponibile anche per .NET Framework 4.7+ e .NET Standard 2.0 come pacchetto NuGet.
La classe TimeProvider definisce le funzionalità seguenti:
- Fornisce l'accesso alla data e all'ora tramite TimeProvider.GetUtcNow() e TimeProvider.GetLocalNow().
- Marca temporale ad alta frequenza con TimeProvider.GetTimestamp().
- Misurare il tempo tra due timestamp con TimeProvider.GetElapsedTime.
- Timer ad alta risoluzione con TimeProvider.CreateTimer(TimerCallback, Object, TimeSpan, TimeSpan).
- Ottenere il fuso orario corrente con TimeProvider.LocalTimeZone.
Implementazione predefinita
.NET offre un'implementazione di TimeProvider tramite la proprietà TimeProvider.System, con le caratteristiche seguenti:
- La data e l'ora vengono calcolate con DateTimeOffset.UtcNow e TimeZoneInfo.Local.
- I timestamp vengono forniti da System.Diagnostics.Stopwatch.
- I timer vengono implementati tramite una classe interna ed esposti come System.Threading.ITimer.
L'esempio seguente illustra l'uso di TimeProvider per ottenere la data e l'ora correnti:
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
L'esempio seguente dimostra come rilevare il tempo trascorso 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
Implementazione di FakeTimeProvider
Il Microsoft.Extensions.TimeProvider.Testing pacchetto NuGet fornisce un'implementazione TimeProvider
controllabile progettata per i test unitari.
L'elenco seguente descrive alcune delle funzionalità della classe FakeTimeProvider:
- Impostare una data e un'ora specifiche.
- Avanzare automaticamente la data e l'ora in base a un importo specificato ogni volta che viene letta la data e l'ora.
- Avanzare manualmente la data e l'ora.
Implementazione personalizzata
Anche se FakeTimeProvider dovrebbe coprire la maggior parte degli scenari che richiedono la prevedibilità con il tempo, è comunque possibile fornire la propria implementazione. Creare una nuova classe che deriva da TimeProvider e sovrascrivere i membri per controllare come viene fornito il tempo. Ad esempio, la classe seguente fornisce solo una singola data, la data dell'atterraggio lunare:
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
Se il codice che usa questa classe chiama MoonLandingTimeProviderPST.GetUtcNow
, viene restituita la data dell'atterraggio lunare in formato UTC. Se MoonLandingTimeProviderPST.GetLocalNow
viene chiamato, la classe base applica MoonLandingTimeProviderPST.LocalTimeZone
a GetUtcNow
e restituisce la data e l'ora di atterraggio lunare nel fuso orario PST.
Per illustrare l'utilità del controllo del tempo, si consideri l'esempio seguente. Si supponga di scrivere un'app calendario che invia un messaggio di saluto all'utente quando l'app viene aperta per la prima volta ogni giorno. L'app dice un saluto speciale quando il giorno corrente ha un evento associato, ad esempio l'anniversario dell'atterraggio lunare.
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
Potrebbe essere preferibile scrivere il codice precedente con DateTime o DateTimeOffset per ottenere la data e l'ora correnti, anziché TimeProvider. Tuttavia, con gli unit test, è difficile aggirare direttamente DateTime o DateTimeOffset. È necessario eseguire i test il giorno e il mese dell'atterraggio lunare o astrarre ulteriormente il codice in unità più piccole ma testabili.
Il normale funzionamento dell'app usa TimeProvider.System
per recuperare la data e l'ora correnti:
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!
Gli unit test possono essere scritti per testare scenari specifici, ad esempio il test dell'anniversario dell'atterraggio lunare:
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!
Usare con .NET
A partire da .NET 8, la classe TimeProvider viene fornita dalla libreria di runtime. Le versioni precedenti di .NET o librerie destinate a .NET Standard 2.0 devono fare riferimento al pacchetto Microsoft.Bcl.TimeProvider pacchetto NuGet.
I metodi seguenti correlati alla programmazione asincrona funzionano con TimeProvider
:
- CancellationTokenSource(TimeSpan, TimeProvider)
- Task.Delay(TimeSpan, TimeProvider)
- Task.Delay(TimeSpan, TimeProvider, CancellationToken)
- Task.WaitAsync(TimeSpan, TimeProvider)
- Task.WaitAsync(TimeSpan, TimeProvider, CancellationToken)
Usare con .NET Framework
TimeProvider viene implementato da Microsoft.Bcl.TimeProvider Pacchetto NuGet.
Il supporto per l'uso di TimeProvider
negli scenari di programmazione asincrona è stato aggiunto tramite i metodi di estensione seguenti:
- TimeProviderTaskExtensions.CreateCancellationTokenSource(TimeProvider, TimeSpan)
- TimeProviderTaskExtensions.Delay(TimeProvider, TimeSpan, CancellationToken)
- TimeProviderTaskExtensions.WaitAsync(Task, TimeSpan, TimeProvider, CancellationToken)
- TimeProviderTaskExtensions.WaitAsync<TResult>(Task<TResult>, TimeSpan, TimeProvider, CancellationToken)