Click here to Skip to main content
15,881,581 members
Articles / Desktop Programming / WPF

.NET DateTime, A Tale About Best Practices and Time Travel

Rate me:
Please Sign up or sign in to vote.
5.00/5 (11 votes)
26 Oct 2020Apache3 min read 8.4K   10   3
In this article, we investigate how DateTime issues are avoided.
Here, we look look at the effect of the daylight saving time (DST) on Local DateTime values, the best practice of using Utc DateTime values, what makes our best practice harder to follow, and how one should specify AssumeLocal (or AssumeUniversal) and AdjustToUniversal.

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.

Image 1

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.

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

C#
//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.

C#
//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:

C#
//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.

Image 2

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:

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

C#
//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.

C#
//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.

C#
//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!

While 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!

License

This article, along with any associated source code and files, is licensed under The Apache License, Version 2.0


Written By
Software Developer (Senior) Microsoft
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
SuggestionDateTimeOffset Pin
Data Quarry27-Oct-20 16:41
professionalData Quarry27-Oct-20 16:41 
QuestionUsing UTC Pin
obermd27-Oct-20 7:53
obermd27-Oct-20 7:53 
GeneralMy vote of 5 Pin
Rusty Bullet27-Oct-20 7:27
Rusty Bullet27-Oct-20 7:27 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.