Every .NET developer has written code that depends on the current time. Token expiry checks, cache invalidation, rate limiters, retry delays, scheduling logic — time is everywhere. And every .NET developer has discovered that DateTime.UtcNow is a static property that you can't mock, swap, or control in tests.
For years, the community answer was to write your own IClock interface. Every team had one. Some used NodaTime's IClock. Others wrapped DateTimeOffset.UtcNow in a service. The pattern was always the same: inject an abstraction, call it in production, fake it in tests.
.NET 8 finally ships a first-class solution: System.TimeProvider.
The problem with static time
Consider a simple token validator:
public bool IsTokenValid(Token token)
{
return token.ExpiresAt > DateTimeOffset.UtcNow;
}
This works in production. But how do you test the case where a token expires in 30 seconds? You could create a token that expires 30 seconds from now and then... wait 30 seconds? Set the expiry to the past and only test the expired path? Neither option tests the boundary correctly, and neither gives you deterministic results.
The root issue: DateTimeOffset.UtcNow is a static call to the system clock. You can't control it, so you can't control your tests.
Enter TimeProvider
System.TimeProvider is an abstract class in the System namespace — no extra packages required for .NET 8+. Here's what it gives you:
public abstract class TimeProvider
{
// The system clock singleton
public static TimeProvider System { get; }
// Current time
public virtual DateTimeOffset GetUtcNow();
public DateTimeOffset GetLocalNow();
// High-resolution timestamps
public virtual long GetTimestamp();
public TimeSpan GetElapsedTime(long startingTimestamp);
// Timer creation
public virtual ITimer CreateTimer(
TimerCallback callback, object? state,
TimeSpan dueTime, TimeSpan period);
// Timezone
public virtual TimeZoneInfo LocalTimeZone { get; }
// Timestamp frequency
public virtual long TimestampFrequency { get; }
}
The key methods are GetUtcNow() for the current time and CreateTimer() for creating timers that respect the provider's notion of time. TimeProvider.System is the default implementation that delegates to the real system clock.
Refactoring to TimeProvider
Let's fix that token validator:
public class TokenValidator
{
private readonly TimeProvider _timeProvider;
public TokenValidator(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}
public bool IsTokenValid(Token token)
{
return token.ExpiresAt > _timeProvider.GetUtcNow();
}
}
Register it in DI:
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddScoped<TokenValidator>();
That's it. Production code uses the real clock. Tests can inject whatever they want.
FakeTimeProvider: Controlled time in tests
The Microsoft.Extensions.TimeProvider.Testing package provides FakeTimeProvider — a test double that gives you complete control over time.
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="9.0.0" />
Tip: This is a test-only package. Reference it from your test project, never from production code.
Basic usage
[Fact]
public void Token_expires_after_threshold()
{
// Arrange — start at a known point in time
var fakeTime = new FakeTimeProvider(
new DateTimeOffset(2026, 1, 15, 10, 0, 0, TimeSpan.Zero));
var token = new Token
{
ExpiresAt = fakeTime.GetUtcNow().AddMinutes(30)
};
var validator = new TokenValidator(fakeTime);
// Act & Assert — token is valid now
Assert.True(validator.IsTokenValid(token));
// Advance time by 29 minutes — still valid
fakeTime.Advance(TimeSpan.FromMinutes(29));
Assert.True(validator.IsTokenValid(token));
// Advance past expiry
fakeTime.Advance(TimeSpan.FromMinutes(2));
Assert.False(validator.IsTokenValid(token));
}
FakeTimeProvider.Advance() moves the clock forward by the specified duration. Time doesn't pass on its own — it only changes when you tell it to. This makes tests completely deterministic.
Setting a specific time
You can set the starting time in the constructor, or adjust it later:
var fakeTime = new FakeTimeProvider();
fakeTime.SetUtcNow(new DateTimeOffset(2026, 6, 15, 0, 0, 0, TimeSpan.Zero));
Auto-advance
For scenarios where you want time to move forward automatically with each call to GetUtcNow():
var fakeTime = new FakeTimeProvider
{
AutoAdvanceAmount = TimeSpan.FromSeconds(5)
};
var t1 = fakeTime.GetUtcNow(); // 00:00:00
var t2 = fakeTime.GetUtcNow(); // 00:00:05
var t3 = fakeTime.GetUtcNow(); // 00:00:10
This is useful for testing code that calls GetUtcNow() multiple times and expects time to have progressed between calls, without manually advancing between each assertion.
A realistic example: cache with expiry
Here's a more practical example — an in-memory cache that expires entries:
public class ExpiringCache<TKey, TValue> where TKey : notnull
{
private readonly ConcurrentDictionary<TKey, CacheEntry> _entries = new();
private readonly TimeProvider _timeProvider;
private readonly TimeSpan _ttl;
public ExpiringCache(TimeProvider timeProvider, TimeSpan ttl)
{
_timeProvider = timeProvider;
_ttl = ttl;
}
public void Set(TKey key, TValue value)
{
_entries[key] = new CacheEntry(value, _timeProvider.GetUtcNow().Add(_ttl));
}
public bool TryGet(TKey key, out TValue? value)
{
if (_entries.TryGetValue(key, out var entry)
&& entry.ExpiresAt > _timeProvider.GetUtcNow())
{
value = entry.Value;
return true;
}
value = default;
_entries.TryRemove(key, out _);
return false;
}
private sealed record CacheEntry(TValue Value, DateTimeOffset ExpiresAt);
}
Testing this without TimeProvider would require either real delays or convoluted workarounds. With FakeTimeProvider, the test is clean and instant:
[Fact]
public void Cache_entry_expires_after_ttl()
{
var fakeTime = new FakeTimeProvider();
var cache = new ExpiringCache<string, int>(fakeTime, TimeSpan.FromMinutes(5));
cache.Set("key", 42);
Assert.True(cache.TryGet("key", out var value));
Assert.Equal(42, value);
// Four minutes later — still cached
fakeTime.Advance(TimeSpan.FromMinutes(4));
Assert.True(cache.TryGet("key", out _));
// Six minutes later — expired
fakeTime.Advance(TimeSpan.FromMinutes(2));
Assert.False(cache.TryGet("key", out _));
}
No Thread.Sleep. No flaky timing. The test runs in microseconds and always produces the same result.
Working with timers
TimeProvider.CreateTimer() returns an ITimer that respects the provider's clock. This is critical for background services that use periodic timers.
public class StatusPoller : IDisposable
{
private readonly ITimer _timer;
private int _pollCount;
public int PollCount => _pollCount;
public StatusPoller(TimeProvider timeProvider, TimeSpan interval)
{
_timer = timeProvider.CreateTimer(
callback: _ => Interlocked.Increment(ref _pollCount),
state: null,
dueTime: interval,
period: interval);
}
public void Dispose() => _timer.Dispose();
}
With FakeTimeProvider, advancing time triggers the timer callbacks:
[Fact]
public void Poller_fires_on_interval()
{
var fakeTime = new FakeTimeProvider();
using var poller = new StatusPoller(fakeTime, TimeSpan.FromSeconds(10));
Assert.Equal(0, poller.PollCount);
fakeTime.Advance(TimeSpan.FromSeconds(10));
Assert.Equal(1, poller.PollCount);
fakeTime.Advance(TimeSpan.FromSeconds(30));
Assert.Equal(4, poller.PollCount);
}
No need to wait for real time to pass. The FakeTimeProvider fires timer callbacks synchronously when you advance past their due time.
High-resolution timestamps
TimeProvider also covers high-resolution timing via GetTimestamp() and GetElapsedTime(). These are useful for measuring durations without allocating Stopwatch instances:
var start = timeProvider.GetTimestamp();
await DoWorkAsync();
var elapsed = timeProvider.GetElapsedTime(start);
logger.LogInformation("Work completed in {Elapsed}", elapsed);
In tests, FakeTimeProvider respects Advance() for timestamps too, so you can verify that your code measures durations correctly.
Registering TimeProvider in dependency injection
The simplest registration:
// Program.cs
builder.Services.AddSingleton(TimeProvider.System);
This registers the real system clock as a singleton. Any class that takes TimeProvider in its constructor will get the real clock in production and can receive FakeTimeProvider in tests.
Important: Some .NET APIs accept TimeProvider directly. For example, Task.Delay has an overload that takes a TimeProvider:
await Task.Delay(TimeSpan.FromSeconds(5), timeProvider);
And CancellationTokenSource can use it for timeouts:
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30), timeProvider);
These overloads make it possible to test timeout and delay behaviour without waiting for real time.
Common pitfalls
Mixing DateTime.UtcNow and TimeProvider
The most common mistake after adopting TimeProvider is forgetting to use it everywhere:
public void Process(Order order)
{
order.ProcessedAt = DateTime.UtcNow; // Oops — not using TimeProvider
if (order.ExpiresAt < _timeProvider.GetUtcNow()) // Using TimeProvider
{
// These two clocks aren't the same in tests!
}
}
Once you introduce TimeProvider, search your codebase for DateTime.Now, DateTime.UtcNow, and DateTimeOffset.UtcNow. Each one is a potential inconsistency in tests.
Not using TimeProvider.System as the default
If your class is sometimes created outside DI, provide a sensible default:
public class RateLimiter
{
private readonly TimeProvider _timeProvider;
public RateLimiter(TimeProvider? timeProvider = null)
{
_timeProvider = timeProvider ?? TimeProvider.System;
}
}
This avoids null reference exceptions and makes the class usable without DI while still being testable.
Creating new Timer() directly
If your code creates System.Threading.Timer directly, FakeTimeProvider can't control it. Always use TimeProvider.CreateTimer() instead:
// Bad — not controllable in tests
var timer = new Timer(callback, null, dueTime, period);
// Good — uses TimeProvider
var timer = timeProvider.CreateTimer(callback, null, dueTime, period);
When not to use TimeProvider
TimeProvider is for application-level time. Don't use it for:
- Benchmarking — use
StopwatchorBenchmarkDotNetdirectly. You want real elapsed time, not a testable abstraction. - Logging timestamps — logging frameworks handle their own timestamps. Adding
TimeProviderto every log call adds complexity with no testing benefit. - Database timestamps — let the database set
created_at/updated_atcolumns via defaults or triggers. Don't push application time into storage.
The rule of thumb: use TimeProvider when the current time affects a decision in your code (is this expired? should we retry? how long has it been?). Don't use it when time is just being recorded.
Summary
TimeProvider removes one of the oldest testing pain points in .NET. Instead of wrapping DateTime.UtcNow in your own interface, you get:
TimeProvider.System— the real clock for productionFakeTimeProvider— a controllable clock for tests, withAdvance(),SetUtcNow(), and auto-advanceCreateTimer()— timers that respect fake time- Framework integration —
Task.Delay,CancellationTokenSource, andPeriodicTimerall acceptTimeProvider
Register TimeProvider.System in DI, inject TimeProvider into your services, and replace every DateTime.UtcNow / DateTimeOffset.UtcNow call. Your tests will be faster, deterministic, and far easier to write.