Time abstraction in .NET
With the new time abstraction support in .NET 8, you'll be able to write more robust and testable code when dealing with time-dependent logic.
The old aproach
Until now, a widely used approach was to create a wrapper to provide the current date and time. Something like the code below:
public interface IDateTimeProvider
{
DateTimeOffset GetCurrentTime();
}
public class SystemDateTimeProvider : IDateTimeProvider
{
public DateTimeOffset GetCurrentTime()
{
return DateTimeOffset.Now;
}
}
And during unit tests, inject a mock implementation of the wrapper to control the behavior of time-related operations.
The new aproach using TimeProvider
Finally, in .NET 8 we have the new TimeProvider
abstract class and the default implementation called SystemTimeProvider
, which can be accessed through the TimeProvider.System
static property.
To use with dependency injection, register TimeProvider.System
as a singleton instance of TimeProvider
.
var services = new ServiceCollection();
services.AddSingleton(TimeProvider.System);
To demonstrate how the new TimeProvider
works, here's a class that depends on the current time:
public class GreetingService
{
private readonly TimeProvider _timeProvider;
public GreetingService(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}
public string GetGreetingMessage()
{
var now = _timeProvider.GetUtcNow();
return now.Hour switch
{
>= 6 and <= 12 => "Good morning!",
> 12 and <= 18 => "Good afternoon!",
> 18 and <= 20 => "Good evening!",
_ => "Good night!"
};
}
}
Writing tests with TimeProvider
For the unit tests, we can use the Microsoft.Extensions.TimeProvider.Testing
library and the FakeTimeProvider
class, which, as the name suggests, is the fake implementation of the TimeProvider
abstract class.
With the FakeTimeProvider
, we can use the SetUtcNow
method to move forward in time (never backwards).
Using xUnit
, this is the unit test for the GreetingService
:
[Theory]
[MemberData(nameof(GreetingServiceUnitTestsCases))]
public void GreetingService_ShouldReturnExpectedGreetingMessage(DateTimeOffset date, string expectedGreetingMessage)
{
// Arrange
_fakeTimeProvider.SetUtcNow(date);
// Act
var result = _greetingService.GetGreetingMessage();
// Assert
Assert.Equal(expectedGreetingMessage, result);
}
And these are the unit test cases:
public static readonly TheoryData<DateTimeOffset, string> GreetingServiceUnitTestsCases = new()
{
{ new DateTimeOffset(year: DateTimeOffset.MaxValue.Year, month: 1, day: 1, hour: 6, minute: 0, second: 0, TimeSpan.Zero), "Good morning!" },
{ new DateTimeOffset(year: DateTimeOffset.MaxValue.Year, month: 1, day: 1, hour: 12, minute: 0, second: 0, TimeSpan.Zero), "Good morning!" },
{ new DateTimeOffset(year: DateTimeOffset.MaxValue.Year, month: 1, day: 1, hour: 13, minute: 0, second: 0, TimeSpan.Zero), "Good afternoon!" },
{ new DateTimeOffset(year: DateTimeOffset.MaxValue.Year, month: 1, day: 1, hour: 18, minute: 0, second: 0, TimeSpan.Zero), "Good afternoon!" },
{ new DateTimeOffset(year: DateTimeOffset.MaxValue.Year, month: 1, day: 1, hour: 19, minute: 0, second: 0, TimeSpan.Zero), "Good evening!" },
{ new DateTimeOffset(year: DateTimeOffset.MaxValue.Year, month: 1, day: 1, hour: 20, minute: 0, second: 0, TimeSpan.Zero), "Good evening!" },
{ new DateTimeOffset(year: DateTimeOffset.MaxValue.Year, month: 1, day: 1, hour: 21, minute: 0, second: 0, TimeSpan.Zero), "Good night!" },
{ new DateTimeOffset(year: DateTimeOffset.MaxValue.Year, month: 1, day: 1, hour: 5, minute: 0, second: 0, TimeSpan.Zero), "Good night!" },
};
Running the unit tests, this is the result (all passed 🥹):
And with that, we finally have a standardization of time abstraction in .NET. 🙌
You can click on the link below to visit my GitHub repository and download the source code for this article.
For more details, click on the link below to access the official documentation.
Member discussion