Key Takeaways
-
Handling date and time correctly in .NET can be a challenging task.
-
DateTimeOffset
, with stored UTC offset and time zone, provides more accuracy for date and time storage than theDateTime
structure. -
Although you can mock external structures like
DateTime
andDateTimeOffset
in unit tests with .NET Fakes, this feature is only available in the Enterprise version of Visual Studio. -
In .NET 8 Preview 4, Microsoft introduced
TimeProvider
andITimer
as universal time abstractions for dependency injections and unit testing. -
TimeProvider
is overloaded with properties and methods, providing extensive functionality for managing time-related operations.
Time plays a critical role in software applications. Tracking time zones and testing time-dependent flows bring challenges to developers. The first part of the article covers the history of .NET Date and Time structures, including existing issues and challenges in time calculation. The second part reviews new .NET 8 Preview 4 abstractions that improve dependency injection and unit testing.
Challenges in utilizing old date and time structures in .NET
DateTime
DateTime has been the main structure for storing date and time in .NET since version 1.1. It has a major drawback - a lack of a time zone. To overcome this problem, the Kind
property was added with three possible values: Local
, Utc
, or Unspecified
.
By default, DateTime.Now
initializes the local time instance with the Kind
property equal to Local
.
var now = DateTime.Now;
Console.WriteLine("Now: {0}", now);
Console.WriteLine("Kind: {0}", now.Kind);
Output:
Now: 06/15/2023 11:00:00
Kind: Local
For the conversion to UTC, it is possible to call the DateTime.Now.ToUniversalTime()
method, or simply use the DateTime.UtcNow
property.
var now = DateTime.Now;
var utc1 = now.ToUniversalTime();
Console.WriteLine("UTC 1: {0}",utc1);
Console.WriteLine("UTC 1 Kind: {0}", utc1.Kind);
var utc2 = DateTime.UtcNow;
Console.WriteLine("UTC 2: {0}", utc2);
Console.WriteLine("UTC 2 Kind: {0}", utc2.Kind);
Output:
UTC 1: 06/15/2023 11:00:00
UTC 1 Kind: Utc
UTC 2: 06/15/2023 11:00:00
UTC 2 Kind: Utc
How does .NET understand the time zone difference during translation from local time to UTC if DateTime
does not provide this information?
The ToUniversalTime
method takes the time zone from the operating system, an approach that might be problematic. Let’s consider a local DateTime.Now
instance that is created in a New York server and then transferred to a London server.
After the conversion to UTC simultaneously on both servers, the results will be different:
var nowInNewYork = DateTime.Now;
var utcInNewYork = nowInNewYork.ToUniversalTime();
Console.WriteLine("UTC in New York: {0}", utcInNewYork);
// Send locally initiated nowInNewYork to a London server
var utcInLondon = nowInNewYork.ToUniversalTime();
Console.WriteLine("UTC in London: {0}", utcInLondon);
Output:
UTC in New York: 06/15/2023 11:00:00
UTC in London: 06/15/2023 07:00:00
This approach led to confusion and frustration for developers; the problems were described in the Microsoft blog along with endless Stackoverflow questions.
On this occasion, Microsoft released the Coding Best Practices Using DateTime in the .NET Framework, shifting all responsibility to the developers:
A developer is responsible for keeping track of time-zone information associated with a DateTime value via some external mechanism. Typically this is accomplished by defining another field or variable that you use to record time-zone information when you store a DateTime value type.
.NET expects time-zone information to be paired with the Kind
property during date and time restoration: DateTimeKind.Local
with TimeZoneInfo.Local
, DateTimeKind.Utc
with TimeZoneInfo.Utc
, and DateTimeKind.Unspecified
for anything else. Custom time zone, as Microsoft recommends, requires to use DateTimeKind.Unspecified
:
var nowInNewYork = DateTime.Now;
Console.WriteLine("New York local time: {0}", nowInNewYork);
var newYorkTimeZone =
TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
nowInNewYork = DateTime.SpecifyKind(nowInNewYork, DateTimeKind.Unspecified);
// Now on any server, in New York or London, UTC is correct
var utc = TimeZoneInfo.ConvertTimeToUtc(nowInNewYork, newYorkTimeZone)
Console.WriteLine("UTC: {0}", utc);
Output:
New York local time: 06/15/2023 12:00:00
UTC: 06/15/2023 16:00:00
An alternative solution may be to create and store time only in the UTC format:
var dtInUtc = DateTime.UtcNow;
then convert it every time to a local time on the user’s side:
var dtLocal = dtInUtc.ToLocalTime();
// or
var dtLocal = TimeZoneInfo.ConvertTimeToUtc(dtInUtc, clientTimeZone);
Unfortunately, this requires additional code checks against accidental use of DateTime
with a local initialization via DateTime.Now
, and also does not exclude such nuances as daylight saving rule changes. I will discuss it in the next section.
DateTimeOffset
As an improvement, the DateTimeOffset
structure was introduced in .NET 2. It consists of:
- structure DateTime
- property Offset storing time difference relative to UTC.
The problem with servers in different time zones will not affect DateTimeOffset.Now
- this is not a panacea for all cases.
Assume there is a need to save an April appointment with a doctor in London. The time zone for London in .NET is TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time")
, passing it to DateTimeOffset
will result in a correct instance, for example, equal to 2023-04-01 14:00:00 with Offset
= +1. Due to daylight saving in Britain, Offset equals +1 at the end of March and equals 0 at the beginning of March. In some countries, there is no daylight saving time. For example, in Nigeria, Offset is always equal to +1.
The daylight saving rules are subject to change, with alternation happening almost every year. It will not be possible to determine the origin of date and time just by looking at the Offset
value, whether it was created for Britain or Nigeria with the same value "01:00:00":
var dt = DateTimeOffset.Now;
Console.WriteLine(dt.Offset);
Output:
01:00:00
Thus, for international software, it is better to store the time zone for possible recalculation of Offset
according to the new rules; TimeZoneInfo
class suits this requirement.
Writing unit tests with time before .NET 8 Preview 4
For unit testing, it is important to be able to mock a method call on an object with a custom implementation. As DateTime
and DateTimeOffset
do not have interfaces, it is possible to create a custom abstraction and later mock them during tests. For example, the following interface can provide an abstraction for DateTimeOffset
:
public interface ISystemClock
{
DateTimeOffset UtcNow { get; }
}
A similar approach was used internally at Microsoft where the same code was added to at least four different areas in .NET.
What about alternative solutions?
Jon Skeet created the NodaTime library with the correct time processing of non-trivial cases, and of course, with the support of abstractions.
Alright, using some custom interface for your code base is possible. Though how can you create integration tests with external libraries that require time transfer via DateTime
and DateTimeOffset
types?
Microsoft has a tool called .NET Fakes that can generate mocks and stubs for any .NET library. For example, it is possible to overwrite all static calls of DateTime.Now
in unit tests with
System.Fakes.ShimDateTime.NowGet = () => { return new
DateTime(2025, 12, 31); };
It works, but there are limitations.
First, the generator is compatible only with Windows OS. Second, at the time of writing, it is included only in the Visual Studio Enterprise version, priced at $250 per month. Third, it is a complex and the most advanced integrated development environment tool from Microsoft. It takes a lot of resources and local storage space, compared to the lightweight Visual Studio Code IDE.
Writing unit tests with time in .NET 8 Preview 4
The long-awaited time abstractions were added after years of debates and hundreds of comments for .NET 8 RC: TimeProvider and ITimer.
public abstract class TimeProvider
{
public static TimeProvider System { get; }
protected TimeProvider()
public virtual DateTimeOffset GetUtcNow()
public DateTimeOffset GetLocalNow()
public virtual TimeZoneInfo LocalTimeZone { get; }
public virtual long TimestampFrequency { get; }
public virtual long GetTimestamp()
public TimeSpan GetElapsedTime(long startingTimestamp)
public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp)
public virtual ITimer CreateTimer(TimerCallback callback, object? state,TimeSpan dueTime, TimeSpan period)
}
public interface ITimer : IDisposable, IAsyncDisposable
{
bool Change(TimeSpan dueTime, TimeSpan period);
}
Ultimately, it did not turn out to be flawless, but this represents significant progress.
TimeProvider disadvantages
1. The abstract class came out bulky. For a method that takes a TimeProvider
argument, it is not possible to decide which method to mock without knowing internal details: GetUtcNow()
, GetLocalNow()
, CreateTimer(...)
, or all of them. Developers proposed to break the new type into small interfaces, in particular, similar to the one already used internally at Microsoft:
public interface ISystemClock
{
public DateTimeOffset GetUtcNow();
}
public abstract class TimeProvider: ISystemClock {
// ...
}
That idea had been rejected.
2. An instance of TimeProvider can be created with the help of a static TimeProvider.System
call. It is very easy to use for a developer, though it is not so different from the old usage of static DateTime.Now
. Later it will result in problems with unit test writing without a special FakeTimeProvider
.
Instead of a direct static call, it is expected that developers will use Dependency Injection for TimeProvider. For example, code for ASP.NET Core can look like
public class MyService
{
public readonly TimeProvider _timeProvider;
public MyService(TimeProvider timeProvider){
_timeProvider = timeProvider;
}
public boolean IsMonday() {
return _timeProvider.GetLocalNow().DayOfWeek == DayOfWeek.Monday;
}
}
// Dependency injection:
var builder = WebApplication.CreateBuilder();
builder.Services.AddSingleton(TimeProvider.System);
builder.Services.AddSingleton<MyService>();
This can be non-trivial for beginner programmers.
TimeProvider and ITimer advantages
1. Time-dependent unit testing becomes more universal. TimeProvider
was added to BCL to support a wide variety of .NET runtimes. It becomes possible to cover the above MyService example with a unit test:
using Moq;
using NUnit.Framework;
[Test]
public void MyTest()
{
var mock = new Mock<TimeProvider>();
mock.Setup(x => x.GetLocalNow()).Returns(new DateTimeOffset(2025, 12, 31, 23, 59, 59, TimeSpan.Zero));
var mockedTimeProvider = mock.Object;
var myService = new MyService(mockedTimeProvider);
var result = myService.IsMonday(mockedTimeProvider);
Assert.IsTrue(result, "Should be Monday");
}
2. The Microsoft team did not bring in the new implementation of the old error where properties had side effects. This is how DateTime.Now
property was mistakenly introduced instead of the DateTime.Now()
function. TimeProvider
abstracts time side effects with functions and methods: GetUtcNow()
, GetLocalNow()
, GetTimestamp()
, etc.
3. It is possible to test time series events with TimeProvider.CreateTimer(...)
and Timer.Change(...)
functions. This is especially important for Task.Delay(...)
and Task.WaitAsync(...)function
calls, which now also accept a TimeProvider
argument.
4. There is a plan to create FakeTimeProvider
as part of .NET to further simplify unit testing. Perhaps then the negative point 2 will not be relevant.
Stephen Toub, a software engineer at Microsoft, wrote:
At the end of the day, we expect almost no one will use anything other than TimeProvider.System in production usage. Unlike many abstractions then, this one is special: it exists purely for testability.
Conclusion
The introduction of the class TimeProvider in .NET 8 Preview 4 specifies a standardized and unified abstraction for managing time. While it may have a few minor drawbacks, Microsoft teams internally marked their custom time interfaces as obsolete and now advocate for adopting TimeProvider
.