Что такое TimeProvider?
System.TimeProvider — это абстракция времени, которая предоставляет точку во времени в виде типа DateTimeOffset. Используя TimeProvider
, вы гарантируете, что код можно тестировать и прогнозировать.
TimeProvider
появилась в .NET 8, а также доступна для .NET Framework 4.7+ и .NET Standard 2.0 в виде пакета NuGet.
Класс TimeProvider определяет следующие возможности:
- Предоставляет доступ к дате и времени через TimeProvider.GetUtcNow() и TimeProvider.GetLocalNow().
- Высокочастотные метки времени с TimeProvider.GetTimestamp().
- Измерение времени между двумя метками времени с помощью TimeProvider.GetElapsedTime.
- Таймеры высокого разрешения с TimeProvider.CreateTimer(TimerCallback, Object, TimeSpan, TimeSpan).
- Определите текущий часовой пояс с помощью TimeProvider.LocalTimeZone.
Реализация по умолчанию
.NET предоставляет реализацию TimeProvider через свойство TimeProvider.System со следующими характеристиками:
- Дата и время вычисляются с помощью DateTimeOffset.UtcNow и TimeZoneInfo.Local.
- Метки времени предоставляются System.Diagnostics.Stopwatch.
- Таймеры реализованы с использованием внутреннего класса и представлены в виде System.Threading.ITimer.
В следующем примере показано использование TimeProvider для получения текущей даты и времени:
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
В следующем примере показано, как измерить истекшее время с помощью 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
Реализация FakeTimeProvider
Пакет NuGet Microsoft.Extensions.TimeProvider.Testing предоставляет управляемую реализацию TimeProvider
, предназначенную для модульного тестирования.
В следующем списке описаны некоторые возможности класса FakeTimeProvider:
- Задайте определенную дату и время.
- Автоматически перемещайте дату и время по указанному количеству при чтении даты и времени.
- Вручную измените дату и время.
Настраиваемая реализация
Хотя FakeTimeProvider должен охватывать большинство сценариев, требующих прогнозируемости во времени, вы по-прежнему можете предоставить собственную реализацию. Создайте новый класс, производный от TimeProvider, и переопределите элементы, чтобы управлять предоставлением времени. Например, следующий класс предоставляет только одну дату, дату посадки луны:
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
Если код, использующий этот класс, вызывает MoonLandingTimeProviderPST.GetUtcNow
, возвращается дата посадки луны в формате UTC. Если вызывается MoonLandingTimeProviderPST.GetLocalNow
, базовый класс применяет MoonLandingTimeProviderPST.LocalTimeZone
к GetUtcNow
и возвращает дату и время высадки на Луну по Тихоокеанскому стандартному времени (PST).
Чтобы продемонстрировать полезность управления временем, рассмотрим следующий пример. Предположим, что вы пишете приложение календаря, которое отправляет приветствие пользователю при первом открытии приложения каждый день. Приложение произносит специальное приветствие, когда с текущим днем связано событие, например, годовщина высадки на Луну.
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
Возможно, вы склонны писать предыдущий код с DateTime или DateTimeOffset, чтобы получить текущую дату и время вместо TimeProvider. Но с модульным тестированием трудно обойти DateTime или DateTimeOffset напрямую. Вам потребуется либо запустить тесты в день и месяц посадки луны, либо дополнительно абстрагировать код в меньшие, но тестируемые единицы.
Обычная операция приложения использует TimeProvider.System
для получения текущей даты и времени:
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!
И модульные тесты можно записать для тестирования конкретных сценариев, таких как тестирование годовщины посадки луны:
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!
Использование с .NET
Начиная с .NET 8, класс TimeProvider предоставляется библиотекой среды выполнения. Более ранние версии .NET или библиотек, предназначенных для .NET Standard 2.0, должны ссылаться на пакете NuGet Microsoft.Bcl.TimeProvider.
Следующие методы, связанные с асинхронным программированием, работают с TimeProvider
:
- CancellationTokenSource(TimeSpan, TimeProvider)
- Task.Delay(TimeSpan, TimeProvider)
- Task.Delay(TimeSpan, TimeProvider, CancellationToken)
- Task.WaitAsync(TimeSpan, TimeProvider)
- Task.WaitAsync(TimeSpan, TimeProvider, CancellationToken)
Использование с .NET Framework
Добавлена поддержка работы с TimeProvider
в сценариях асинхронного программирования с помощью следующих методов расширения:
- TimeProviderTaskExtensions.CreateCancellationTokenSource(TimeProvider, TimeSpan)
- TimeProviderTaskExtensions.Delay(TimeProvider, TimeSpan, CancellationToken)
- TimeProviderTaskExtensions.WaitAsync(Task, TimeSpan, TimeProvider, CancellationToken)
- TimeProviderTaskExtensions.WaitAsync<TResult>(Task<TResult>, TimeSpan, TimeProvider, CancellationToken)