TimeProvider — Конец Нетестируемого DateTime.Now. Начало
DateTime.Now — один из тех вызовов, который кажется безобидным, пока вы не попытаетесь написать тест вокруг него. Это статическое свойство, которое считывает системные часы, и нет способа контролировать то, что оно возвращает, без обёртывания его во что-то, что можно внедрить. Начиная с .NET 8, мы можем перестать переизобретать одну и ту же абстракцию и использовать вместо неё TimeProvider.
Как было раньше
Стандартным решением для установки определённого времени в коде всегда было определение интерфейса, который оборачивает часы, и внедрение его в качестве зависимости, которую можно заменить фиктивной реализацией в тестах, зависящих от даты/времени:
public interface IDateTimeProvider
{
DateTimeOffset UtcNow { get; }
}
public class SystemDateTimeProvider : IDateTimeProvider
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}
В коде вы регистрируете SystemDateTimeProvider. В тестах — FakeDateTimeProvider с устанавливаемым свойством и передаёте любое время, необходимое тесту. Это работает и несложно, но каждая команда делает это немного по-своему: ISystemClock, IClock, IDateTimeProvider, ITimeService или сразу несколько вариантов в одной кодовой базе, потому что, а почему бы и нет? ASP.NET Core одно время поставлял свой ISystemClock в Microsoft.AspNetCore.Authentication.
Встроенное решение: TimeProvider
.NET 8 представил TimeProvider, абстрактный класс в пространстве имен System. Он является частью базовой библиотеки классов (поэтому зависимость NuGet не требуется). Идея та же: вместо прямого вызова DateTime.UtcNow вы вызываете tp.GetUtcNow(). В коде используется TimeProvider.System. В тестах используется фиктивный объект. Основные методы:
// Текущее время UTC (DateTimeOffset, а не DateTime)
DateTimeOffset utcNow = tp.GetUtcNow();
// Местное время
DateTimeOffset localNow = tp.GetLocalNow();
// Местный часовой пояс
TimeZoneInfo zone = tp.LocalTimeZone;
// Высокоточные метки времени
long start = tp.GetTimestamp();
// ... do work ...
TimeSpan elapsed = tp.GetElapsedTime(start);
Заметьте, что TimeProvider работает с DateTimeOffset (содержащим информацию о смещении часового пояса), а не с DateTime.
В коде используется TimeProvider.System как синглтон. Он считывает данные с системных часов, как и DateTimeOffset.UtcNow. Его необходимо зарегистрировать при запуске системы:
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddSingleton<TimeProvider>(TimeProvider.System);
А затем можно внедрять везде, где вашему коду нужно текущее время:
public class SubscriptionReminderService
{
private TimeProvider _tp;
private ISubscriptionRepository _repo;
public SubscriptionReminderService(
TimeProvider tp,
ISubscriptionRepository repo)
{
_tp = tp;
_repo = repo;
}
public async Task<IEnumerable<Subscription>>
GetExpiringSubsAsync()
{
var now = _tp.GetUtcNow();
var cutoff = now.AddDays(7);
return await
_repo.GetExpiringSubsAsync(cutoff);
}
}
Теперь часы являются зависимостью, а зависимости можно заменять. Например, в тестах.
Окончание следует…
Источник: https://blog.maartenballiauw.be/posts/2026-07-22-timeprovider-and-the-end-of-untestable-datetime-now/