#TLDR

Handling dates/times in software is deceptively hard. If handling dates, times and zones extensively/accurately is a core concern (you will know if it is), use NodaTime. For the rest of us, the built in .NET classes will usually suffice if you know how they work. If you fall into the latter camp, read this brief post to gain a working knowledge of DateTime and DateTimeOffset in .NET.

Background

.NET provides classes for handling dates/times which generally work as well as most other programming languages but they can leave a lot to be desired if you need to take full control. That’s why Jon Skeet created NodaTime.

While NodaTime is a technically superior framework for handling dates/times and more accurately exposes the complexities involved, this open source project still isn’t free. All that power makes it more difficult to understand and implement dates/times for anyone who hasn’t used it before which adds a hidden cost over time. In my particular use case, I decided it didn’t warrant the full power/complexity of NodaTime and the built-in .NET classes would work for us if we understood them.

If you are interested in a more comprehensive explanation of the shortcomings of date/time handling in .NET read this excellent post by Jon Skeet.

NOTE – This blog post and my findings assume .NET Core 2.1.

DateTime

The DateTime class represents a date and time value but does not include any time offset. It does have a Kind property which indicates whether the DateTime is a Local, UTC or Unspecified instance.

You can use the Now and UtcNow static properties to get a local or UTC DateTime respectively:

Or you can use the DateTime constructor overloads to create a DateTime. If no Kind is specified it defaults to Unspecified:

DateTimeOffset

The DateTimeOffset class represents a date and time with an offset. Similar to DateTime, it has Now and UtcNow static properties as well as constructor overloads for setting the offset explicitly:

Notice there are also methods to create a new DateTimeOffset converted to local, universal or a custom offset time from an existing DateTimeOffset.

The offset value is set from a TimeSpan value which assumes you know the appropriate offset for a specific time zone.

Daylight Savings Time

When time zone information is available (more details below), DateTimeOffset will adjust to the appropriate DST offset.

Converting from DateTime to DateTimeOffset (don’t miss this!)

There is an implicit conversion from DateTime to DateTimeOffset which can be convenient but can also cause confusion if you don’t know how it works. The implicit conversion will assign the offset based on the DateTimeKind on the DateTime.

Be careful that you don’t pass a DateTime assuming it will default to a UTC offset. Instead explicitly create DateTime with the correct kind OR the DateTimeOffset with the appropriate offset value using the constructor shown in the earlier section.

TimeZoneInfo

.NET does also have a TimeZoneInfo class which provides time zone details. On Windows the time zone info is based on the time zone info in the Windows registry. On Linux, the time zone info uses a different standard and is only optionally installed (its not installed in many scenarios). Read this StackOverflow post for more details.

You can also lookup time zones by their ID:

Lessons Learned

In the end we were able to use the built-in classes for handling dates/times in our use case successfully after digging into the framework and understanding what is available and how it works.

Here are a few lessons we learned and practices we will employ going forward to avoid issues:

Unit Test

Unit test your date/time code to verify it will work from and across any time zones. Test with multiple offsets to simulate different servers running your code. Remember your local machine will usually have a different default time zone than the server where the code will ultimately run.

Store DateTimes in UTC

You never know where your code will run. Especially, as more code moves to the cloud you never know where the server will be geographically or the default time zone it will be configured with. Whenever possible use UTC everywhere you can, then you always know the source and convert back to the needed timezone/offset.

Be Explicit

Don’t assume the framework will work the way you want. Explicitly convert DateTimes to DateTimeOffsets with the appropriate offset value to avoid odd behavior.

Feedback

I hope this post helps others who are struggling with date/time issues. If you find it useful or have further suggestions, please leave a comment below. Thanks!