Yet more "fun" with time zones in .NET

Yet more "fun" with time zones in .NET

It is a truth universally acknowledged, that a .NET project in possession of multiple time zones, must be in want of Noda Time. Sometimes though, we have an existing project where it's just not reasonable to move everything over and we need a way to display a DateTime in a time zone that may not necessarily be the local time zone.

While .NET does include such types as DateTimeOffset, an offset for UTC is not the same as a timezone. Time zones can and do change, including daylight saving changes. An offset from UTC does not reflect that and can be shared across many time zones.

For instance, if we created a DateTimeOffset with a UTC offset of +01:00, that could be Paris in winter (Central European Time), Dublin in summer (Irish Standard Time) or Lagos in Nigeria (West Africa Time) at any time of year since daylight saving is not observed there.

Suppose instead that we have a system where the DateTime has been saved as UTC and we now want to display it as a local time zone. If we have a given string tzId representing our time zone then we can do the following...

DateTime utc = DateTime.UtcNow;

DateTime local =
   TimeZoneInfo.ConvertTimeFromUtc(utc,
       TimeZoneInfo.FindSystemTimeZoneById(tzId));

If you're running .NET Framework then you're restricted to Windows and you can use the Windows time zones defined in the registry at HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\.

If you're running .NET Core then it gets a little more complicated.

On Windows hosts then it still uses the registry entries above, where for example Dublin, Ireland is represented as "GMT Standard Time". On Linux and macOS it uses the tzdata format of "Europe/Dublin".

This does make things awkward at the moment, particularly in Azure functions where you don't know what platform your code is going to be running on.

If you know the time zone in advance you may consider something like

DateTime local;
try {
  //This is for Linux
  local = TimeZoneInfo.ConvertTimeFromUtc(utc,
              TimeZoneInfo.FindSystemTimeZoneById("Europe/Dublin"));
}
catch (TimeZoneNotFoundException) {
  //This is for Windows
  local = TimeZoneInfo.ConvertTimeFromUtc(utc,
              TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time"));
}
Console.WriteLine(local);

Hardly elegant code.

One option is to use the TimeZoneConverter library and combine it with a check of what platform we are executing on

// using System.Runtime.InteropServices;
// using TimeZoneConverter;

public static DateTime GetLocalTime(DateTime utc, string timeZone)
{

    bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
    if (isWindows) {
        timeZone = TZConvert.IanaToWindows(timeZone);
        // There is also a TZConvert.WindowsToIana method for going the other way
    }
    
    var local =
        TimeZoneInfo.ConvertTimeFromUtc(utc,
            TimeZoneInfo.FindSystemTimeZoneById(timeZone));
    
    return local;
}

var utc = DateTime.UtcNow; // 2020-06-23 07:50
var timeZone = "Europe/Dublin";

var local = GetLocalTime(utc, timeZone); // 2020-06-23 08:50

So there is a way around but time zones are still a bit of a mess when dealing with cross-platform code. There is a GitHub issue for aligning time zone identifiers across platforms in .NET Core so check it out and give a thumbs up if it would help you out.