3 min read

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.

Microsoft.Extensions.TimeProvider.Testing 8.1.0
Hand-crafted fakes to make time-related testing easier.

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.

GitHub - cristiano-bonassina/blog__dotnet_time_abstraction
Contribute to cristiano-bonassina/blog__dotnet_time_abstraction development by creating an account on GitHub.

For more details, click on the link below to access the official documentation.

What’s new in .NET 8 runtime
Learn about the new .NET features introduced in the .NET 8 runtime.