Calling DateTime.UtcNow directly inside business logic makes the current time an invisible dependency. Tests then have to race the real clock, calculate values relative to whatever time the test happens to run, or avoid important boundaries such as expiration entirely.
Inject TimeProvider when the behavior depends on what time it is. Register the system implementation in production:
builder.Services.AddSingleton<TimeProvider>(TimeProvider.System);
The application code can now ask for the current UTC time without owning the clock:
public sealed class SubscriptionPolicy(TimeProvider timeProvider)
{
public bool IsExpired(DateTimeOffset expiresAt) =>
timeProvider.GetUtcNow() >= expiresAt;
}
For tests, install Microsoft.Extensions.TimeProvider.Testing and pass a FakeTimeProvider. The test controls exactly when the subscription expires:
using Microsoft.Extensions.Time.Testing;
var clock = new FakeTimeProvider(
new DateTimeOffset(2026, 7, 20, 8, 0, 0, TimeSpan.Zero));
var policy = new SubscriptionPolicy(clock);
var expiresAt = clock.GetUtcNow().AddHours(1);
Assert.False(policy.IsExpired(expiresAt));
clock.Advance(TimeSpan.FromHours(1));
Assert.True(policy.IsExpired(expiresAt));
No delay or tolerance window is needed. The same approach works for timer-based behavior because TimeProvider also supports timestamps, elapsed-time measurement, and timers.
Do not inject a clock into every type by default. A timestamp that has already been created is data and should usually be passed as a DateTimeOffset. Use TimeProvider at the boundary where application behavior needs to decide what “now” means. Keep business logic on UTC instants and convert to local time only where the user-facing context requires it.