runtime: Eliminate ambiguous date and time values

aka The Date project

Background and Motivation

Dotnet Core contains several types for representing dates and times. As noted in the guidance documentation, developers should “consider DateTimeOffset as the default date and time type for application development” (https://docs.microsoft.com/en-us/dotnet/standard/datetime/choosing-between-datetime)

However in many cases, in examples, component packages, and business software, DateTime is used, even though it is the incorrect type – part of this is historical because DateTime existed prior to DateTimeOffset. This issue even affects Dotnet and many examples from Microsoft, where DateTime is often used where DateTimeOffset would be more appropriate.

This proposal - Everywhere that an ambigious DateTime has been used in Dotnet, it should be supplemented with an unambiguous DateTimeOffset, and these values should be promoted as the default for future use.

It is currently difficult to follow the guidance advice and use DateTimeOffset when basic classes like FileInfo only use DateTime.

There are still some uses for DateTime. For some, such as working with UTC dates and times, DateTimeOffset can also be used, and should still be considered the default, or for working with times only then TimeSpan can be used.

The cases where DateTime is the correct class are:

  • working with dates only, for example a birthdate for calculating something like drinking age only the date part is relevant – it is generally not relevant which timezone you were born in or your current timezone.

  • working with abstract dates and times, for example in a calendar application; a common example is having a meeting at 09:00 on the first of each month, where the instance of time will vary in line with timezones. In these cases the date and time components need to be treated separately.

In both these cases, only the date part is used, not the full DateTime, and would be better served by a Date only structure; for all other cases either DateTimeOffset or another type (TimeSpan) is more appropriate. In some cases (e.g. calendar) both a Date and TimeSpan value would be needed, but also need to be handled independently (and not combined into a single DateTime value).

Promoting the use of Date, for date only, and DateTimeOffset, for other scenarios, will help eliminate the issues that can arise from using ambiguous dates, e.g. where a field is supposed to be date only bug a bug has introduce a time component into the DateTime field – such a bug is not possible with a date only structure.

The long term goal is to eventually be able to mark DateTime as Obsolete – something that I have proposed before.

Proposed API

The new API changes would consist of adding a Date struct, and adding DateTimeOffset properties through Dotnet to supplement where there is currently only a DateTime.

To implement unambiguous single points of time across the entire Dotnet is a large, long term project, that can be done incrementally, with the following roadmap:

  1. Implement a Date structure for those scenarios that require only date. This will eliminate the need to ambiguously use DateTime with a time component, where only date is needed.

A basic structure can be introduced, and then incrementally added to as needed.

  1. Incrementally add supplementary DateTimeOffset properties anywhere that DateTime is used in Dotnet. This would allow the guidance of “consider DateTimeOffset as the default” to be acted upon.

In some cases these values may already exist, e.g. FileSystemInfo already uses DateTimeOffset internally but just doesn’t expose it in the API.

Searching the code for something like “[\s.()=±/!&[]{}]DateTime[\s.()=±/!&[]{}]” within .cs files gives an idea of the scope – 10,000 references across 1,000 files.

  1. Remove the dependency from DateTimeOffset on DateTime. Internally DateTimeOffset uses DateTime, plus the offset. This should be changed to a ulong, and make DateTimeOffset a stand alone structure without dependency on DateTIme limited to conversions.

Initially, this would involve duplication of some code from DateTime to DateTimeOffset (there is also duplication in some places like comdatetime). Further incremental changes may reverse the dependency, e.g. DateTime could call an IsValidTimeWithLeapSeconds in DateTimeOffset, to then remove the duplication.

  1. As an additional aid, any example code, training material, documentation, and other guidance can also be incrementally updated, e.g. any examples that reference FileInfo should be updated to show examples using DateTimeOffset fields, not date time.

Steps 1-4 can be done in parallel, incrementally.

  1. Once all functionality has been moved across, places where DateTime is used can be incrementally marked Obsolete and/or hidden from editors (e.g. Intellisense), via [EditorBrowsable(EditorBrowsableState.Never)].

Note that marking something Obsolete is not a directly breaking change, the API is the same and does not break any runtime dependencies. There may, however, be some indirect issues when recompiling any dependencies if “treat warnings as errors” is turned on – these would be good indicators to fix those dependencies to also use the DateTimeOffset alternative. You could potentially start this before steps 1-4 are complete, but it may be clearer to wait.

  1. Eventually, DateTime itself can be marked Obsolete, and/or hidden, having been fully replaced by DateTimeOffset and, in some cases, Date.

There may be opposition to marking DateTime Obsolete, due to the possibility of breaking build systems that have treat warnings as errors turned on (not a runtime breaking change), but if all instances within Dotnet have been replaced, and after sufficient time, this should be possible. Once this is achieved, ambiguous dates and times will have been eliminated from Dotnet.

A compromise may be to hide the class from editors, which will not break builds but help encourage future developers follow the guidance and use DateTimeOffset.

Note: I have previously raised a suggestion to mark DateTime as Obsolete.

Usage Examples

The overview example of How to get information about files, folders, and drives, (https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/file-system/how-to-get-information-about-files-folders-and-drives) would be changed to use the DateTimeOffset version of the property.

        foreach (System.IO.FileInfo fi in fileNames)
        {
            Console.WriteLine("{0}: {1}: {2}", fi.Name, fi.LastAccessAt, fi.Length);
        }

The corresponding current properties on FileInfo (FileSystemInfo) are LastAccessTime and LastAccessTimeUtc. The specific pattern used for DateTimeOffset properties can be discussed and agreed.

Other possible variations could be LastAccess, LastAccessTimeAt, LastAccessedAt, or LastAccessedTimeAt. Another possible variation is a past tense form LastAccessed similar to the IFileInfo interface from Microsoft.Extensions.FileProviders, although this could be confused with the conventions for event names.

Alternative Designs

The main well known alternative is probably Noda Time (https://nodatime.org/).

Risks

There is probably a lot of opposition to trying to replace a core construct such as DateTime, although the professional guidance is that in most cases DateTimeOffset is far more appropriate to use.

Marking it Obsolete could also indirectly cause issues with build processes in some cases, although it does not break runtime API compatibility.

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 4
  • Comments: 25 (19 by maintainers)

Most upvoted comments

… we’ve had System.Time.Date and System.Time.Time types in corefxlab for years, although they’ve never been pulled in.

Personally, I rather wish that we’d get a good, complete, first-party date/time library, like I proposed years ago. For one thing, it allows representations much closer to conceptual/semantic than the limited types you’ve proposed. Although the migration plan you’ve outlined here is essentially what I imagined.

Note that DateTimeOffset is often not the correct semantic type - normally you want either an equivalent to NodaTime’s Instant or DateTimeZoned (it often ends up being the correct “solution” type, since it’s what’s available). For instance, file access times should really be represented as an Instant (since any zone, including UTC, is irrelevant).