Skip to content

Instantly share code, notes, and snippets.

@byme8
Last active September 4, 2024 09:06
Show Gist options
  • Save byme8/b58af9bed32ffd2d4b5638e95c6dee9c to your computer and use it in GitHub Desktop.
Save byme8/b58af9bed32ffd2d4b5638e95c6dee9c to your computer and use it in GitHub Desktop.

Why DateTime is a problem

If you’ve ever worked on a C# application that handles dates and times, chances are you’ve run into some frustrating issues with DateTime. It might seem simple at first—just store the date and time, right? But as your app grows and starts dealing with users across different time zones, things can get messy fast.

A Simple Example

Let's start with a simple example. Your app has to create an appointment and show it to the user on some webpage. The model for it may look like that:

public class AppointmentEntity
{
    public int Id { get; set; }

    public string Description { get; set; }

    public string Address { get; set; }

    public DateTime CreateDate { get; set; }
    public DateTime? UpdateDate { get; set; }

    public DateTime Start { get; set; }
    public DateTime End { get; set; }
}

At first glance, this code seems straightforward. You might encounter it in a PR, and with a quick glance, you might even approve it. However, as someone who has been bitten by such simplicity before, I’d now say, “Hold on, there are numerous ways this could break.” Once the application is deployed to production, it’s only a matter of time before customers start complaining that they missed appointments due to incorrect times being displayed. When you open a ticket and try to reproduce the issue, it might go something like this:

  1. You will open the ticket, read the steps to reproduce. You create new appointment for 27-12-2024 at 10-11.
  2. You check the database to see the following:
{
    "id": 1,
    "description": "Meet with John",
    "address": "Main 1 Street",

    "createDate": "25-12-2024T12:23:52.1434",
    "updateDate": null,
    "start": "27-12-2024T10:00:00",
    "end": "27-12-2024T11:00:00",
}
  1. You open the app and see the correct dates. Everything seems fine, so you mark the ticket as cannot reproduce and move on.

But then, the same issue keeps popping up repeatedly.

After some investigation, you realize that the appointment was created in one time zone but read in another. Let’s try again, this time considering time zones.

  1. You create a new appointment for 27-12-2024 at 10:00-11:00 in a UTC+2 time zone.
  2. You check the database and see:
{
    "id": 1,
    "description": "Meet with John",
    "address": "Main 1 Street",

    "createDate": "25-12-2024T12:23:52.1434",
    "updateDate": null,
    "start": "27-12-2024T10:00:00",
    "end": "27-12-2024T11:00:00",
}
  1. When you open the app in the UTC+2 time zone, the dates look correct. However, if you open the app in a UTC+1 time zone, the appointment time has shifted, now displaying 27-12-2024 at 09:00-10:00.

Okay, the issue is reproduced. But what caused it? The answer is complex; there isn’t just one cause. Here are a few common culprits:

  • The database uses the datetime2 type, which ignores time zones.
  • When deserializing the DateTime defaults to an “unspecified” time zone.
  • Someone configured API-level serialization to convert everything to UTC. When it encounters an “unspecified” time zone, it assumes UTC.
  • The client app treats everything as UTC.

Each of these can cause the problem, and while you might temporarily fix it by standardizing everything under one approach, a new team member might inadvertently reintroduce the issue by doing something differently. And the cycle repeats.

A Better Approach

For such cases we can use the NodaTime nuget package. It offers a more robust and explicit way to handle date and time in your application, helping to avoid the common pitfalls associated with the built-in DateTime and DateTimeOffset types. You can install it via:

dotnet add package NodaTime 

Once we installed the package, let's have a look at redefined appointment entity via the NodaTime:

public class AppointmentEntity
{
    public int Id { get; set; }

    public string Description { get; set; }

    public string Address { get; set; }

    public Instant CreateDate { get; set; }
    public Instant? UpdateDate { get; set; }

    public LocalDateTime Start { get; set; }
    public LocalDateTime End { get; set; }
}

NodaTime introduces distinct types for different time-related concepts, which makes your intent clear and reduces the chance of errors.

The Instant - represents a specific point in time, always in UTC. It is an unambiguous, timezone-independent timestamp. When you use Instant, you know that the value is not tied to any local time zone. This is perfect for fields like CreateDate and UpdateDate, where you need to record the exact moment an event occurred, irrespective of the user's location.

public Instant CreateDate { get; set; }
public Instant? UpdateDate { get; set; }

By using Instant for these fields, you ensure that the creation and update times are stored consistently across all time zones. There is no ambiguity about what time was recorded, and this timestamp can be reliably compared or used for sorting.

The LocalDateTime - represents a date and time without a time zone. It is perfect for scenarios where the time is meaningful within a specific context, such as a meeting or appointment. When you schedule an appointment for "10:00 AM on December 27, 2024," what you mean is 10:00 AM in the time zone where the event will occur, regardless of the time zone of the person creating or viewing the appointment.

public LocalDateTime Start { get; set; }
public LocalDateTime End { get; set; }

By using LocalDateTime, you convey that the start and end times are meant to be interpreted within a specific time zone, typically associated with the event's location (like an office or a city). This prevents the common mistake of inadvertently shifting appointment times when they are viewed or manipulated in different time zones.

Why This Matters

The distinction between Instant and LocalDateTime addresses a major flaw in the way DateTime works. DateTime can represent a moment in time, but without additional context (like a time zone), it's easy to misuse. For example, a DateTime value can be "unspecified," meaning it doesn’t know whether it’s local time, UTC, or something else. This ambiguity often leads to bugs, especially in distributed systems where time zone differences are common. Effectivly, NodaTime forces you to be explicit when converting between different time representations. If you need to display a LocalDateTime in UTC, you must explicitly convert it, ensuring you consider the context of the time zone. This prevents accidental errors that often occur with DateTime, where it’s easy to misinterpret or mishandle the time zone information.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment