Guidance for TimeProvider abstraction for testable time-dependent code. USE FOR: making time-dependent code testable, replacing DateTime.UtcNow and DateTimeOffset.UtcNow with injectable abstractions, controlling time in unit tests with FakeTimeProvider, testing expiration logic, scheduling, token lifetimes, and time-based business rules. DO NOT USE FOR: high-precision timing or benchmarking (use Stopwatch), NTP synchronization, or scenarios running on .NET versions prior to .NET 8.
Guidance for TimeProvider abstraction for testable time-dependent code. USE FOR: making time-dependent code testable, replacing DateTime.UtcNow and DateTimeOffset.UtcNow with injectable abstractions, controlling time in unit tests with FakeTimeProvider, testing expiration logic, scheduling, token lifetimes, and time-based business rules. DO NOT USE FOR: high-precision timing or benchmarking (use Stopwatch), NTP synchronization, or scenarios running on .NET versions prior to .NET 8.
[{"title":"TimeProvider Class Documentation","url":"https://learn.microsoft.com/en-us/dotnet/standard/datetime/timeprovider-overview"},{"title":"Microsoft.Extensions.TimeProvider.Testing NuGet Package","url":"https://www.nuget.org/packages/Microsoft.Extensions.TimeProvider.Testing"}]
TimeProvider
Overview
TimeProvider is a .NET 8+ abstraction that centralizes time access (, , ) and timer creation behind an injectable class. In production, you use which delegates to the system clock. In tests, you use from to freeze, advance, and control time deterministically. This eliminates the common anti-pattern of calling or directly in business logic, which makes time-dependent code untestable without real-time waits.
GetUtcNow()
GetLocalNow()
GetTimestamp()
TimeProvider.System
FakeTimeProvider
Microsoft.Extensions.TimeProvider.Testing
DateTime.UtcNow
DateTimeOffset.UtcNow
Service Registration
Register TimeProvider in the DI container for production use.
using Microsoft.Extensions.DependencyInjection;
// Program.cs - register the system clock
builder.Services.AddSingleton(TimeProvider.System);
// Alternatively, register as the abstract type
builder.Services.AddSingleton<TimeProvider>(TimeProvider.System);
Injecting TimeProvider in Services
Replace DateTime.UtcNow and DateTimeOffset.UtcNow with TimeProvider.
publicsealedclassTokenService
{
privatereadonly TimeProvider _timeProvider;
privatereadonly TimeSpan _tokenLifetime = TimeSpan.FromHours(1);
publicTokenService(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}
public Token GenerateToken(string userId)
{
var now = _timeProvider.GetUtcNow();
returnnew Token
{
UserId = userId,
IssuedAt = now,
ExpiresAt = now.Add(_tokenLifetime),
Value = Guid.NewGuid().ToString("N")
};
}
publicboolIsTokenValid(Token token)
{
var now = _timeProvider.GetUtcNow();
return token.ExpiresAt > now;
}
}
publicrecordToken
{
publicstring UserId { get; init; } = string.Empty;
public DateTimeOffset IssuedAt { get; init; }
public DateTimeOffset ExpiresAt { get; init; }
publicstring Value { get; init; } = string.Empty;
}
Testing with FakeTimeProvider
Use FakeTimeProvider to control time in unit tests.
using Microsoft.Extensions.Time.Testing;
using Xunit;
publicclassTokenServiceTests
{
[Fact]
publicvoidGenerateToken_Sets_Correct_Expiry()
{
// Arrange: freeze time at a known pointvar fakeTime = new FakeTimeProvider(
new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero));
var service = new TokenService(fakeTime);
// Actvar token = service.GenerateToken("user-1");
// Assert: token expires exactly 1 hour from "now"
Assert.Equal(
new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero),
token.IssuedAt);
Assert.Equal(
new DateTimeOffset(2025, 6, 15, 13, 0, 0, TimeSpan.Zero),
token.ExpiresAt);
}
[Fact]
publicvoidToken_Is_Valid_Before_Expiry()
{
var fakeTime = new FakeTimeProvider(
new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero));
var service = new TokenService(fakeTime);
var token = service.GenerateToken("user-1");
// Advance time by 30 minutes (still within 1 hour lifetime)
fakeTime.Advance(TimeSpan.FromMinutes(30));
Assert.True(service.IsTokenValid(token));
}
[Fact]
publicvoidToken_Is_Invalid_After_Expiry()
{
var fakeTime = new FakeTimeProvider(
new DateTimeOffset(2025, 6, 15, 12, 0, 0, TimeSpan.Zero));
var service = new TokenService(fakeTime);
var token = service.GenerateToken("user-1");
// Advance time past the 1 hour expiry
fakeTime.Advance(TimeSpan.FromHours(2));
Assert.False(service.IsTokenValid(token));
}
}
Time-Based Business Rules
Test complex time-dependent business logic deterministically.
publicsealedclassSubscriptionService
{
privatereadonly TimeProvider _timeProvider;
publicSubscriptionService(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}
public SubscriptionStatus GetStatus(Subscription subscription)
{
var now = _timeProvider.GetUtcNow();
if (now < subscription.StartDate)
return SubscriptionStatus.Pending;
if (now > subscription.EndDate)
return SubscriptionStatus.Expired;
if (subscription.EndDate - now <= TimeSpan.FromDays(7))
return SubscriptionStatus.ExpiringSoon;
return SubscriptionStatus.Active;
}
publicboolIsInTrialPeriod(Subscription subscription)
{
var now = _timeProvider.GetUtcNow();
var trialEnd = subscription.StartDate.AddDays(14);
return now >= subscription.StartDate && now <= trialEnd;
}
publicintDaysRemaining(Subscription subscription)
{
var now = _timeProvider.GetUtcNow();
var remaining = subscription.EndDate - now;
return Math.Max(0, (int)remaining.TotalDays);
}
}
publicrecordSubscription
{
public DateTimeOffset StartDate { get; init; }
public DateTimeOffset EndDate { get; init; }
publicstring Plan { get; init; } = string.Empty;
}
publicenum SubscriptionStatus
{
Pending,
Active,
ExpiringSoon,
Expired
}
Testing Business Rules
Test each subscription status transition with controlled time.
using Microsoft.Extensions.Time.Testing;
using Xunit;
publicclassSubscriptionServiceTests
{
privatereadonly FakeTimeProvider _fakeTime;
privatereadonly SubscriptionService _service;
privatereadonly Subscription _subscription;
publicSubscriptionServiceTests()
{
_fakeTime = new FakeTimeProvider(
new DateTimeOffset(2025, 1, 15, 0, 0, 0, TimeSpan.Zero));
_service = new SubscriptionService(_fakeTime);
_subscription = new Subscription
{
StartDate = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero),
EndDate = new DateTimeOffset(2025, 12, 31, 23, 59, 59, TimeSpan.Zero),
Plan = "Annual"
};
}
[Fact]
publicvoidStatus_Is_Pending_Before_Start()
{
// Set time to before subscription start
_fakeTime.SetUtcNow(
new DateTimeOffset(2024, 12, 25, 0, 0, 0, TimeSpan.Zero));
Assert.Equal(SubscriptionStatus.Pending,
_service.GetStatus(_subscription));
}
[Fact]
publicvoidStatus_Is_Active_During_Subscription()
{
Assert.Equal(SubscriptionStatus.Active,
_service.GetStatus(_subscription));
}
[Fact]
publicvoidStatus_Is_ExpiringeSoon_Within_7_Days()
{
_fakeTime.SetUtcNow(
new DateTimeOffset(2025, 12, 28, 0, 0, 0, TimeSpan.Zero));
Assert.Equal(SubscriptionStatus.ExpiringeSoon,
_service.GetStatus(_subscription));
}
[Fact]
publicvoidStatus_Is_Expired_After_End()
{
_fakeTime.SetUtcNow(
new DateTimeOffset(2026, 1, 2, 0, 0, 0, TimeSpan.Zero));
Assert.Equal(SubscriptionStatus.Expired,
_service.GetStatus(_subscription));
}
[Fact]
publicvoidTrial_Period_Is_First_14_Days()
{
// Day 10: still in trial
_fakeTime.SetUtcNow(
new DateTimeOffset(2025, 1, 10, 0, 0, 0, TimeSpan.Zero));
Assert.True(_service.IsInTrialPeriod(_subscription));
// Day 20: past trial
_fakeTime.SetUtcNow(
new DateTimeOffset(2025, 1, 20, 0, 0, 0, TimeSpan.Zero));
Assert.False(_service.IsInTrialPeriod(_subscription));
}
[Fact]
publicvoidDaysRemaining_Decreases_Over_Time()
{
_fakeTime.SetUtcNow(
new DateTimeOffset(2025, 12, 1, 0, 0, 0, TimeSpan.Zero));
int remaining = _service.DaysRemaining(_subscription);
Assert.Equal(30, remaining);
_fakeTime.Advance(TimeSpan.FromDays(15));
remaining = _service.DaysRemaining(_subscription);
Assert.Equal(15, remaining);
}
}
Timer Testing with FakeTimeProvider
Test timer-based logic without real delays.
using Microsoft.Extensions.Time.Testing;
using Xunit;
publicsealedclassPollingService : IDisposable
{
privatereadonly TimeProvider _timeProvider;
privatereadonly ITimer _timer;
privateint _pollCount;
publicint PollCount => _pollCount;
publicPollingService(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
_timer = timeProvider.CreateTimer(
callback: _ => Interlocked.Increment(ref _pollCount),
state: null,
dueTime: TimeSpan.Zero,
period: TimeSpan.FromSeconds(30));
}
publicvoidDispose() => _timer.Dispose();
}
publicclassPollingServiceTests
{
[Fact]
publicvoidTimer_Fires_On_Schedule()
{
var fakeTime = new FakeTimeProvider();
usingvar service = new PollingService(fakeTime);
// Initial callback fires immediately
Assert.Equal(1, service.PollCount);
// Advance 30 seconds: second callback
fakeTime.Advance(TimeSpan.FromSeconds(30));
Assert.Equal(2, service.PollCount);
// Advance another 60 seconds: two more callbacks
fakeTime.Advance(TimeSpan.FromSeconds(60));
Assert.Equal(4, service.PollCount);
}
}
TimeProvider API Comparison
API
TimeProvider.System
FakeTimeProvider
GetUtcNow()
Real system clock
Controlled value
GetLocalNow()
Real local time
Controlled + timezone
GetTimestamp()
Real high-res counter
Controlled counter
CreateTimer()
Real timer with delays
Fires on Advance()
Thread safety
Yes
Yes
Requires .NET 8+
Yes
Yes
Best Practices
Never call DateTime.UtcNow or DateTimeOffset.UtcNow directly in application code: always inject TimeProvider and call GetUtcNow() so time can be controlled in tests.
Register TimeProvider.System as a singleton: the system time provider is stateless and thread-safe; register it once at the composition root.
Use FakeTimeProvider.Advance() to simulate time progression: advance by specific durations to test expiration, scheduling, and timeout logic without real waits.
Use FakeTimeProvider.SetUtcNow() to jump to specific points in time: for testing scenarios like "what happens on December 31st", set the exact date rather than calculating an advance duration.
Initialize FakeTimeProvider with a known starting time: pass an explicit DateTimeOffset to the constructor (new FakeTimeProvider(startTime)) so test assertions are deterministic and readable.
Test timer callbacks by advancing FakeTimeProvider: timers created via timeProvider.CreateTimer() fire synchronously when Advance() crosses their due time, enabling timer testing without Task.Delay.
Use TimeProvider for all time-related operations in a class: if a class uses both GetUtcNow() and CreateTimer(), both should come from the same injected TimeProvider instance for consistent behavior in tests.
Test boundary conditions around time transitions: verify behavior at exactly the expiration moment, one millisecond before, and one millisecond after to catch off-by-one errors in time comparisons.
Avoid mixing TimeProvider with direct Task.Delay calls: use timeProvider.CreateTimer() or Task.Delay(duration, timeProvider) (via extension methods) so delays are also controllable in tests.
Install Microsoft.Extensions.TimeProvider.Testing only in test projects: the base TimeProvider class is built into .NET 8+; the FakeTimeProvider package is a test-only dependency.