.NET DateTime, a tale about best practices and time travel

As a .NET developer, I am used to the .NET class library being really well architected.

This is a double edged sword because, when DateTime, one of the most used types in any application, starts misbehaving, it may catch you by surprise.

Horloge retour vers le futur.jpg

1985, Hill Valley CA

If you want to follow along the code samples in this post, make sure to set your computer’s time zone to Pacific Time (US & Canada).

Photo by Søren Lundtoft, marked as Free Use

Anybody, who had to suffer through using dates and times in Java, likely appreciates the ease of having a single DateTime class in .NET representing both Utc and Local times, as well as the mysterious “Unspecified” times.

Unfortunately the DateTime type shows some very unexpected and error prone behavior when working with anything except for Utc times.

Always use Utc DateTime values!
Local DateTime values should be limited to the UI layer only.

The temporal displacement occurred exactly 1:00 AM and zero seconds

I will start this post by looking at the effect of the daylight saving time (DST) on Local DateTime values.

In the code snippets below, I will show the full content of each DateTime value as a comment. You can use this extension method to achieve the same formatting.

public static class DateTimeUtils
{
  public static void Print(this DateTime t) =>
    Console.WriteLine(t.ToString("yyyy-MM-dd HH:mm") + " " +
                      t.Kind + " " + 
                      (t.IsDaylightSavingTime() ? "DST" : "ST"));
}

The following timestamps are 30 minutes before and after the DST change in Pacific Time. Because you turn the clock back one hour when leaving DST, both their local times are 1:30AM.

//1985-10-27 08:30 Utc ST
var utc1 = new DateTime(1985, 10, 27, 8, 30, 0, DateTimeKind.Utc);
//1985-10-27 09:30 Utc ST
var utc2 = new DateTime(1985, 10, 27, 9, 30, 0, DateTimeKind.Utc);

//1985-10-27 01:30 Local DST
var local1 = utc1.ToLocalTime();
//1985-10-27 01:30 Local ST
var local2 = utc2.ToLocalTime();

So utc1 and utc2 are 1 hour apart and the same is true for local1 and local2 because they represent the same instants. No information is lost in the conversion to local time: local1 and local2 can be correctly converted back to UTC.

//1985-10-27 08:30 Utc ST
local1.ToUniversalTime();
//1985-10-27 09:30 Utc ST
local2.ToUniversalTime();

Things get weird when we start to use operators. Both equality and arithmetic operators work well for Utc times but are broken for Local times or a mix of the two:

//Utc
Console.WriteLine(utc1 == utc2); //False
Console.WriteLine(utc2 - utc1); //01:00:00

//Local
Console.WriteLine(local1 == local2); //True (wrong!)
Console.WriteLine(local2 - local1); //00:00:00 (wrong!)

//Mix
Console.WriteLine(local1 - utc1); //-07:00:00 (wrong!)

This behavior is partially documented but it is nonetheless extremely error-prone:

The Equality operator determines whether two DateTime values are equal by comparing their number of ticks. Before comparing DateTime objects, make sure that the objects represent times in the same time zone. You can do this by comparing the values of their Kind property.

Great Scott!

Most of these inconsistencies are only triggered for two hours in the whole year, during the DST changes. They are very likely to be missed by unit tests or QA.

Image by Emanhattan, used under Creative Commons license

Best practices

DateTime issues are avoided with one, easy to follow, best practice: only use Utc DateTime values!

The use of Local DateTime values should be limited to the UI layer only. If you are using WPF or some other UI framework which supports data binding, you could consider using Utc all the way up to the UI and bake the conversion to/from Local into the data biding itself.

In a previous post, I even played around with the idea of not using DateTime at all and replace it with a compatible type that only allows Utc times:

public struct UtcDateTime
{
  private readonly DateTime Time;
   
  public UtcDateTime(DateTime time)
  {
    switch (time.Kind)
    {
      case DateTimeKind.Utc:
        Time = time;
        break;
      case DateTimeKind.Local:
        Time = time.ToUniversalTime();
        break;
      default:
        throw new NotSupportedException("UtcDateTime cannot be initialized with an Unspecified DateTime.");
    }
  }
   
  public static implicit operator UtcDateTime(DateTime t) => new UtcDateTime(t);
  public static implicit operator DateTime(UtcDateTime t) => t.Time;

  //Add implementation for operators and other utility methods
}

DatETime parsing gives us back the wrong 1985

(In which Biff is corrupt, and powerful...)

What makes our best practice harder to follow is that the Parse and ParseExact methods have a tendency to return Local, or even Unspecified, DateTime values, even when the input string represents a Utc time.

//Parsing a string can give us an Unspecified DateTime.
//1985-10-27 01:30 Unspecified ST
DateTime.Parse("1985-10-27T01:30:00", CultureInfo.InvariantCulture);

//Parsing a string will give us a Local DateTime even if we specify that
//the string represents a Utc time!
//At least the conversion is correct.
//1985-10-27 01:30 Local DST
DateTime.Parse("1985-10-27T08:30:00", CultureInfo.InvariantCulture,
                                      DateTimeStyles.AssumeUniversal);

//Parsing a string will give us a Local DateTime even if the string is
//explicitly marked as Utc ("Z").
//Again, the conversion to Local is correct.
//1985-10-27 01:30 Local DST
DateTime.Parse("1985-10-27T08:30:00Z", CultureInfo.InvariantCulture);

//ParseExact may return an Unspecified value even if the string is
//explicitly marked as Utc ("Z"). This is because the format string is
//slightly off: "\\Z" instead of "Z".
//1985-10-27 08:30 Unspecified ST (wrong!)
DateTime.ParseExact("1985-10-27T08:30:00Z", "yyyy-MM-ddTHH:mm:ss\\Z",
                                            CultureInfo.InvariantCulture);
//1985-10-27 01:30 Local DST
DateTime.ParseExact("1985-10-27T08:30:00Z", "yyyy-MM-ddTHH:mm:ssZ",
                                            CultureInfo.InvariantCulture);

I particularly dislike using Unspecified DateTime values because behave inconsistently when converted to Local and Utc.

//1985-10-27 08:30 Unspecified ST
var t = new DateTime(1985, 10, 27, 8, 30, 0);

//Behaves like Utc when converted to Local
//1985-10-27 01:30 Local DST
t.ToLocalTime();

//Behaves like Local when converted to UTc
//1985-10-27 16:30 Utc ST
t.ToUniversalTime();

The best practice to avoid all the complexity around parsing DateTime values is to always specify AssumeLocal (or AssumeUniversal if it is more appropriate for your use case) and AdjustToUniversal. This makes parsing behave much more consistently and always return Utc values that are ready to be stored or used across the application.

//Thanks to AdjustToUniversal (in conjunction with AssumeLocal or
//AssumeUniversal), returned values are consistently Utc!

//AdjustToUniversal alone is not enough to guarantee a Utc result.
//1985-10-27 01:30 Unspecified ST
DateTime.Parse("1985-10-27T01:30:00", CultureInfo.InvariantCulture,
                                      DateTimeStyles.AdjustToUniversal);
//AssumeLocal takes care of avoiding Unspecified values being returned.
//AssumeUniversal works in a similar way.
//1985-10-27 09:30 Utc ST
DateTime.Parse("1985-10-27T01:30:00", CultureInfo.InvariantCulture,
                                      DateTimeStyles.AssumeLocal |
                                      DateTimeStyles.AdjustToUniversal);
//1985-10-27 08:30 Utc ST
DateTime.Parse("1985-10-27T08:30:00", CultureInfo.InvariantCulture,
                                      DateTimeStyles.AssumeUniversal |
                                      DateTimeStyles.AdjustToUniversal);

//If the string being parsed is marked as Utc (or has a specific timezone)
//AssumeLocal is ignored. This is good!
//1985-10-27 08:30 Utc ST
DateTime.Parse("1985-10-27T08:30:00Z", CultureInfo.InvariantCulture,
                                       DateTimeStyles.AssumeLocal |
                                       DateTimeStyles.AdjustToUniversal);

//We still have the problem with ParseExact, if the format string is
//incorrect.
//1985-10-27 16:30 Utc ST (wrong!)
DateTime.ParseExact("1985-10-27T08:30:00Z", "yyyy-MM-ddTHH:mm:ss\\Z",
                                            CultureInfo.InvariantCulture,
                                            DateTimeStyles.AssumeLocal |
                                            DateTimeStyles.AdjustToUniversal);
//Using AssumeUniversal works around the problem.
//1985-10-27 08:30 Utc ST
DateTime.ParseExact("1985-10-27T08:30:00Z", "yyyy-MM-ddTHH:mm:ss\\Z",
                                            CultureInfo.InvariantCulture,
                                            DateTimeStyles.AssumeUniversal |
                                            DateTimeStyles.AdjustToUniversal);

Whoa, This is heavy!

Wile all of this is pretty basic stuff, plenty of experienced engineers, me included, are tripped by this from time to time. The effects can be pretty bad, including crashes and data loss or corruption.

Hopefully you found this information useful, and scary enough to remember to follow the best practices :-).

Thanks for reading.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s