Working with Dates and Times
Introduction
Our platform is meant to be a highly available multi-region app, meaning it can and will run multiple instances on multiple timezones at the same time. On top of this, it is also meant to be accessed by many different users in many different timezones. With this in mind, there are a few problems we must consider:
Problem #1 - People
The first problem starts with people: since most live their lives within a single time zone and mostly interact with others who do the same, they forget that expressions like the day after tomorrow refer to specific points in time, that vary across the globe in representation:
19/09/2025 00:00 PT (UTC+1)
18/09/2025 23:00 Coordinated Universal Time (UTC+0)
18/09/2025 19:00 NY (UTC-4)
All represent the same instant in time, despite appearing different. This can be especially confusing when people ignore the time component and focus only on the date. In our earlier example, the day after tomorrow doesn’t specify a time, but since we need to interpret it as a specific moment, we default to midnight, but have no idea what the person actually meant.
Problem #2 - Daylight Saving Time
To add to the confusion, time zones aren't fixed due to Daylight Saving Time. For example, in Portugal, DST begins on the last Sunday in March and ends on the last Sunday in October. This means that for the same country and time zone, the phrase tomorrow at 1 in the morning can be ambiguous, depending on whether DST is in effect.
Imagine an event what happens every day between 23:00 and 03:00. On the night the clock turns backwards, how many hours did this event last? It depends
Problem #3 - Inconsistencies
Every layer of our solution have their own "rules":
- The database we use, MongoDB, stores times in UTC by default.
- Our APIs, if they instance a new date property, will use the local time zone of the server they are running on.
- Our front-end client can use the user's local time zone or the server it run on time zone, depending on what mode it is running at.
Our solution
There are two main ways to handle dates in C#, DateTime and DateTimeOffset. Let's examine their advantages and limitations:
DateTime
In essence, DateTime represents the number of ticks since epoch (January 1, 0001 in our case). It does not hold any information about time zones. It does store a Kind property that indicates whether the time is based on local time, UTC or neither. This can be enough if, in every layer, the time zone never changes.
For our solution, this won't be enough. The Front-End and Back-End layers can vary both between themselves and across instances of each layer. Imagine a scenario where the User is in Portugal (UTC+1) and sends a DateTime with Local Kind to a API in France (UTC+2):
Since the Kind is Local, it assumes the time zone of the system running the code. Once they differ, it loses the difference between them.
Another limitation is that its operators use the Ticks property., meaning to comparisons between the same date in Local and UTC will return unexpected results:
//2020-10-02 00:00 - PT (UTC+1)
DateTime local = new(year: 2020, month: 10, day: 02, hour: 00, minute: 00, second: 00, kind: DateTimeKind.Local);
//2020-10-01 23:00 - UTC
DateTime utc = new(year: 2020, month: 10, day: 01, hour: 23, minute: 00, second: 00, kind: DateTimeKind.Utc);
bool areDateTimesEqual = local == utc;
Console.WriteLine("areDateTimesEqual: " + areDateTimesEqual);
// The example displays the following output:
// areDateTimesEqual: false
This can be a problem if you want to compare a date retrieved from a database against a date instantiated in the API, for example, a business rule ensuring you cannot create a document with past dates:
//This will return false
if (contract.Date > DateTime.Now)
//In order to to this properly you would need to convert them both to UTC before:
//This will return true
if (contract.Date.ToUniversalTime() > DateTime.Now.ToUniversalTime());
All of this means, to use DateTime properly in our scenario, we would need to always convert every instance of it to Universal Time. Which is impractical and very easy to forget when developing.
DateTimeOffset
This is the Type we use since it is basically a DateTime value together with an Offset property that defines the difference between the current DateTimeOffset instance's date and time and UTC.
At every layer, we maintain the Offset property, converting correctly between them no matter where in the world they are. Its operators also use UTCDateTime instead of Ticks, solving the comparison issue from before.
//2020-10-02 00:00 - PT (UTC+1)
DateTimeOffset localOffset = new(year: 2020, month: 10, day: 02, hour: 00, minute: 00, second: 00, offset: new TimeSpan(1, 0, 0));
//2020-10-01 23:00 - UTC
DateTimeOffset utcOffset = new(year: 2020, month: 10, day: 01, hour: 23, minute: 00, second: 00, offset: new TimeSpan(0, 0, 0));
bool areDateTimeOffsetsEqual = localOffset == utcOffset;
Console.WriteLine("areDateTimeOffsetsEqual: " + areDateTimeOffsetsEqual);
// The example displays the following output:
// areDateTimeOffsetsEqual: true
BrowserTimeProvider
With our Back-End issues solved, we now need to tackle the problem that Blazor presents when running server-side, instead of client-side: Its time zone is depended on the server it runs on. This means that if a user instantiates, manipulates or is presented with a date, it will assume the server's time zone instead of their computer, causing issues if they differ.
To mitigate this we implemented the BrowserTimeProvider middleware and TimeProviderExtensions that can read and convert dates from and to the user's time zone (we call this Browser Time). This is used anywhere the user is shown dates.