In this article, we’ll look at TimeProvider – a new feature coming to C# 12 and .NET 8 that provides a built-in way to deal with and test time-dependent code.

To download the source code for this article, you can visit our GitHub repository.

Let’s get started!

What Is TimeProvider in .NET?

The abstract class TimeProvider is a new addition to C# 12 and .NET 8. It provides the means for time abstraction, which enables time mocking in test scenarios. This abstraction supports essential time operations such as retrieving local and UTC time. We can also obtain a timestamp for performance measurement and create timers.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

We also get a class called FakeTimeProvider added to the Microsoft.Extensions.Time.Testing namespace. As the name suggests, it can be used to mock the functionality of the actual TimeProvider class.

To be able to use the FakeTimeProvider class, we need to install its package:

dotnet add package Microsoft.Extensions.TimeProvider.Testing --version 8.0.0-rc.1.23453.1

Now, we are good to go!

What Problem Does TimeProvider Solve?

Let’s imagine we have the following DiscountService class:

public class DiscountService : IDiscountService
{
    public double CalculateDiscount()
    {
        var now = DateTime.UtcNow;

        return now.DayOfWeek switch
        {
            DayOfWeek.Monday => 1,
            DayOfWeek.Tuesday => 2,
            DayOfWeek.Wednesday => 3,
            DayOfWeek.Thursday => 4,
            DayOfWeek.Friday => 5,
            DayOfWeek.Saturday => 6,
            DayOfWeek.Sunday => 7,
            _ => 0
        };
    }
}

We create a DiscountService class that has one method called CalculateDiscount(). It is responsible for calculating a discount based on the current day of the week. The method also utilizes DateTime.UtcNow to get the current date and time. However, there is a major problem with this approach – our code is not testable.

One popular approach for testing code that depends on DateTime or DateTimeOffset is to create an interface for a date and time provider and then implement it.

Let’s see how this works:

public interface IDateTimeProvider
{
    DateTime UtcNow { get; }
}

public class DateTimeProvider : IDateTimeProvider
{
    public DateTime UtcNow => DateTime.UtcNow;
}

We create a IDateTimeProvider interface that has one property of DateTime type called UtcNow. After that we implement the interface in the DateTimeProvider class and assign the value of DateTime.UtcNow to the UtcNow property.

Now, we can inject the IDateTimeProvider in our service and use it in our CalculateDiscount() method. With this done, we can mock the IDateTimeProvider in our tests, using a mocking library such as NSubstitute, and adjusting the UtcNow property to return different days of the week in each test method.

How Does TimeProvider Solve the Problem?

Let’s update our service to use TimeProvider:

public class DiscountService : IDiscountService
{
    private readonly TimeProvider _timeProvider;

    public DiscountService(TimeProvider timeProvider)
    {
        _timeProvider = timeProvider;
    }

    public double CalculateDiscount()
    {
        var now = _timeProvider.GetUtcNow();

        return now.DayOfWeek switch
        {
            DayOfWeek.Monday => 1,
            DayOfWeek.Tuesday => 2,
            DayOfWeek.Wednesday => 3,
            DayOfWeek.Thursday => 4,
            DayOfWeek.Friday => 5,
            DayOfWeek.Saturday => 6,
            DayOfWeek.Sunday => 7,
            _ => 0
        };
    }
}

We inject TimeProvider in our constructor and assign it to a private field. Then in the CalculateDiscount() method, we invoke the GetUtcNow() method on the _timeProvider field to get the current time. The rest of our code doesn’t need any updates and its functionality remains the same.

There is one more thing we need to do:

builder.Services.AddSingleton(TimeProvider.System);

In our Program class, we register the TimeProvider provided by the static property System as a singleton. Using this built-in TimeProvider the time of the system is used on which our code is running, which means it uses the hardware clock.

We can now move on to testing our code.

Testing Time Dependent Code With TimeProvider in .NET

Using the xUnit framework, we create our test class:

public class DiscountServiceTests : IDisposable
{
    private readonly FakeTimeProvider _timeProvider;
    private readonly DiscountService _discountService;

    public DiscountServiceTests()
    {
        _timeProvider = new FakeTimeProvider();
        _discountService = new DiscountService(_timeProvider);
    }
}

We define the DiscountServiceTests class with two private, read-only fields.

The first is _timeProvider, which is an instance of the FakeTimeProvider class, and _discountService, which is an instance of our DiscountService class we want to test. In the constructor of our test class, we create and assign new instances to those fields. Note that we can inject the FakeTimeProvider instance in the constructor of the DiscountService directly as it implements the TimeProvider class.

Next, we move on to our tests and create a test method to verify that we get the proper discount for Mondays:

[Fact]
public void GivenMonday_WhenCalculateDiscountIsInvoked_ThenValidDiscountIsReturned()
{
    // Arrange
    _timeProvider.SetUtcNow(new DateTime(2023, 5, 1));

    // Act
    var result = _discountService.CalculateDiscount();

    // Assert
    result.Should().Be(1);
}

In the arrange part we use the SetUtcNow() method of the FakeTimeProvider class to set the expected result of the GetUtcNow() method. We pass it a DateTime representation of May 1st, 2023 which was a Monday. Afterwards we invoke the CalculateDiscount() method and assert that the result it returns is equal to 1. The same approach works for the rest of the cases, we just need to pass dates that correspond to the desired weekdays.

How Can TimeProvider in .NET Help With Timers ?

We can use TimeProvider to create a timer:

public class DiscountService : IDiscountService, IDisposable
{
    private readonly ITimer _timer;
    private readonly TimeProvider _timeProvider;
    private double _specialDiscount = 0;
    private bool _disposed = false;

    public double SpecialDiscount
    { 
        get => _specialDiscount;
        set => _specialDiscount = value;
    }

    public DiscountService(TimeProvider timeProvider)
    {
        _timeProvider = timeProvider;

        _timer = _timeProvider.CreateTimer(
            _ => UpdateSpecialDiscount(),
            state: null,
            dueTime: TimeSpan.FromSeconds(5),
            period: TimeSpan.FromHours(1));
    }
}

First, we create the SpecialDiscount property and set it to zero. Then, we add an ITimer field and initialize it in the constructor using the TimeProvider‘s CreateTimer() method. We pass four parameters to the method – the first one is the callback, or what happens each time the timer hits its mark. In our case, we pass the UpdateSpecialDiscount() method. Then we pass null as the state, we set the timer to fire for the first time 5 seconds after initialization and then to fire every hour.

Next, we create the UpdateSpecialDiscount() method:

private void UpdateSpecialDiscount()
{
    var timeOfDay = _timeProvider.GetUtcNow().TimeOfDay;

    if (timeOfDay < TimeSpan.FromHours(6))
    {
        Interlocked.Exchange(ref _specialDiscount, 5);
    }
    else if (timeOfDay >= TimeSpan.FromHours(6) &&
                timeOfDay < TimeSpan.FromHours(12))
    {
        Interlocked.Exchange(ref _specialDiscount, 4);
    }
    else if (timeOfDay >= TimeSpan.FromHours(12) &&
                timeOfDay < TimeSpan.FromHours(18))
    {
        Interlocked.Exchange(ref _specialDiscount, 3);
    }
    else
    {
        Interlocked.Exchange(ref _specialDiscount, 2);
    }
}

The method is responsible for setting a specific value for the SpecialDiscount property that uses the backing field _specialDiscount. If the time is between midnight and 05:59, we get 5%, if it is between 06:00 and 11:59 we get 4%, and so on. This method utilizes Interlocked.Exchange() to avoid potential issues caused by the asynchronous value update.

To test the behavior, we can use a built-in functionality of the FakeTimeProvider:

[Theory]
[InlineData(0, 5.0)]
[InlineData(6, 4.0)]
[InlineData(12, 3.0)]
[InlineData(18, 2.0)]
[InlineData(24, 5.0)]
public void WhenTimePasses_ThenSpecialDiscountIsUpdatedAccordingly(int hoursToAdvance, double expectedDiscount)
{
    //Act
    _discountService.SpecialDiscount.Should().Be(0);
    _timeProvider.SetUtcNow(new DateTime(2023, 5, 1, 0, 0, 0));

    //Arrange
    _timeProvider.Advance(TimeSpan.FromHours(hoursToAdvance) + TimeSpan.FromSeconds(10));

    // Assert
    _discountService.SpecialDiscount.Should().Be(expectedDiscount);
}

In our test class, we create a new method that will verify that as time passes the SpecialDiscount changes accordingly.

Firstly we are asserting that SpecialDiscount is equal to zero, meaning that no time has passed and that the timer has not fired yet. Secondly, we set the time to midnight on May 1st by using SetUtcNow() method and use the Advance() method of the FakeTimeProvider to move the time by the number of hours in the argument hoursToAdvance plus an additional 10 seconds. After this is done, we check that the discount percentage has been set to the expectedDiscount defined by our InlineData() attribute.

That’s it. Consequently, this is how we can now easily test our time-dependent code without a lot of work.

Conclusion

In this article, we learned about TimeProvider which is a valuable addition to C# and .NET, empowering us to write more robust and testable code when dealing with time-dependent logic. This abstract class, along with its companion class FakeTimeProvider, allows developers to abstract time-related operations, making it easier to mock time in test scenarios.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!