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.
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.
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.