Make Time Testable with TimeProvider
We've all been there. You write a unit test that depends on DateTime.UtcNow, and suddenly you're building a custom IClock interface, or wrapping things in a static ambient context, or just... hoping the test doesn't run at midnight on a leap year. TimeProvider puts all of that to rest. It's a first-class abstraction for time, and it lives right in the base class library.
The concept is straightforward. Instead of calling DateTime.UtcNow or DateTimeOffset.UtcNow directly, you inject a TimeProvider and call GetUtcNow(). In production, you register TimeProvider.System, which just returns the real clock. In tests, you swap in a FakeTimeProvider that lets you set, freeze, and advance time whenever you want.
Let's look at a practical example. Say you have a service that checks whether a coupon code has expired:
public class CouponService(TimeProvider timeProvider)
{
public bool IsValid(Coupon coupon)
{
var now = timeProvider.GetUtcNow();
return now < coupon.ExpiresAt;
}
public async Task WaitAndExpire(Coupon coupon, CancellationToken ct)
{
var remaining = coupon.ExpiresAt - timeProvider.GetUtcNow();
if (remaining > TimeSpan.Zero)
{
await Task.Delay(remaining, timeProvider, ct);
}
coupon.IsActive = false;
}
}
Notice that Task.Delay overload. It accepts a TimeProvider, so even delays become fully controllable in tests. Registering it in your DI container works the same way as any other service:
builder.Services.AddSingleton(TimeProvider.System);
Now for the fun part. Install the testing companion package:
dotnet add package Microsoft.Extensions.TimeProvider.Testing
And write tests that are completely deterministic. No flaky timing, no Thread.Sleep, no crossing your fingers:
using Microsoft.Extensions.Time.Testing;
public class CouponServiceTests
{
[Fact]
public void IsValid_ReturnsFalse_AfterExpiration()
{
// Start at a known point in time
var fake = new FakeTimeProvider(
new DateTimeOffset(2025, 6, 1, 12, 0, 0, TimeSpan.Zero));
var coupon = new Coupon
{
ExpiresAt = fake.GetUtcNow().AddHours(2)
};
var service = new CouponService(fake);
Assert.True(service.IsValid(coupon));
// Jump forward 3 hours, instantly
fake.Advance(TimeSpan.FromHours(3));
Assert.False(service.IsValid(coupon));
}
[Fact]
public async Task WaitAndExpire_DeactivatesCoupon_WhenTimeAdvances()
{
var fake = new FakeTimeProvider(
new DateTimeOffset(2025, 6, 1, 12, 0, 0, TimeSpan.Zero));
var coupon = new Coupon
{
ExpiresAt = fake.GetUtcNow().AddMinutes(30),
IsActive = true
};
var service = new CouponService(fake);
var task = service.WaitAndExpire(coupon, CancellationToken.None);
// Time hasn't moved, coupon is still active
Assert.True(coupon.IsActive);
// Advance past expiration
fake.Advance(TimeSpan.FromMinutes(31));
await task;
Assert.False(coupon.IsActive);
}
}
public class Coupon
{
public DateTimeOffset ExpiresAt { get; set; }
public bool IsActive { get; set; }
}
The FakeTimeProvider.Advance() call is the key method. It moves the clock forward instantly, and any pending Task.Delay calls waiting on that TimeProvider will resolve right away. No real waiting, no race conditions, no flaky CI runs.
If you're still maintaining your own IClock or IDateTimeProvider interface, it's time to retire it. TimeProvider is build into .NET, works with Task.Delay, PeriodicTimer, and CancellationTokenSource.CancelAfter, and has a purpose-built fake implementation that is ready to go.