In C#, we have several tools for modeling time. At first, everyone uses DateTime, but as we progress we discover the world is more complex (time zones, durations, dates without time…).
So C# provides a pleeethora of Classes and methods aimed at managing entities related to time. In summary:
| Situation | Recommended Type | Why |
|---|---|---|
| General Use / Web Apps | DateTimeOffset | Unambiguous, includes time zone. |
| Legacy / In-memory only | DateTime (UTC) | Compatible with old libraries. |
| Measuring elapsed time | TimeSpan | Date mathematics. |
| Birthdays / Anniversaries | DateOnly | We don’t need time or time zone. |
| Alarm clock / Schedules | TimeOnly | Not tied to a specific day. |
Before delving into the different classes, let’s review the topic of dates in computers.
What is time for a computer? (Ticks)
For C#, a date is not “October 12th”. It’s a number (for your computer, everything is numbers, even letters).
In .NET, time is measured in Ticks.
A Tick is the smallest unit of time the system can represent. It equals 100 nanoseconds (yes, nanoseconds. That’s how precise it is).
When we create a date, internally C# stores a 64-bit integer (long) that counts how many Ticks have passed since January 1, year 0001 at 00:00:00.
The Common Form DateTime
The DateTime structure is the most common way to represent an instant in time. It combines date and time.
// Create a specific date (Year, Month, Day, Hour, Minute, Second)
DateTime releaseDate = new DateTime(2025, 12, 25, 10, 30, 00);
// Get the current date
DateTime now = DateTime.Now;
The Context Problem (DateTimeKind)
Here comes the important point. A DateTime has a property called Kind that indicates what that date represents:
Utc: Coordinated Universal Time (the global standard, “Zulu time”).Local: The time of the computer where the code runs (with its time zone and daylight saving).Unspecified: We only know the date and time, but not from where or which time zone.
Always save and process in UTC (DateTime.UtcNow). Convert to Local only to display it to the end user.
Do not save dates to the database using DateTime.Now (Local). If your server is in Spain and your user is in Mexico, you’ll have a monumental mess.
The Modern Form DateTimeOffset
If you are making a web or cloud application, forget DateTime. Use the improved DateTimeOffset.
The problem with DateTime is that it’s ambiguous. If it’s 10:00, is that 10:00 in London or Tokyo?
DateTimeOffset stores two things:
- The exact instant (UTC).
- The offset (Offset) from UTC for the local zone.
DateTimeOffset exactMoment = DateTimeOffset.Now;
// Prints something like: 15/08/2023 10:30:00 +02:00
With this structure, we know the event occurred at 10:30 local time, and that locality is 2 hours ahead of the Greenwich meridian. It’s an absolute and unambiguous representation of time.
Microsoft officially recommends using DateTimeOffset as the default type for modern application development, unless you have a very specific reason to use DateTime.
The Duration TimeSpan
While DateTime and DateTimeOffset are points on the timeline (a “when?”), TimeSpan is a length of time (a “how long?”).
It represents an interval: For example 2 hours, 5 minutes, and 10 seconds.
We can get a TimeSpan by subtracting two DateTimes:
DateTime start = new DateTime(2023, 1, 1);
DateTime end = new DateTime(2023, 1, 10);
TimeSpan duration = end - start;
Console.WriteLine(duration.TotalDays); // 9.0
Console.WriteLine(duration.TotalHours); // 216.0
We can also create them manually to add time to a date:
TimeSpan halfHour = TimeSpan.FromMinutes(30);
DateTime newDate = DateTime.Now + halfHour; // Advance 30 mins
DateOnly and TimeOnly
For years, if we wanted to save a birth date (without time) we used DateTime and set the time to 00:00:00. Or if we wanted to save an alarm time (without date), we used an arbitrary DateTime or a TimeSpan.
It was a mess waste of memory and semantically incorrect.
Since .NET 6, we have DateOnly and TimeOnly.
DateOnly: Stores only day, month, and year. Uses less memory (internally uses a 32-bit integer, not 64). Ideal for birthdays or holidays.TimeOnly: Stores only the clock time. Ideal for store opening hours or recurring alarms.
DateOnly birthday = new DateOnly(1990, 5, 20);
TimeOnly opening = new TimeOnly(09, 00);
Formatting and Parsing
To display a date to the user, we use the .ToString() method with format strings.
Dates depend on Culture (CultureInfo). The format in Spain (dd/MM/yyyy) is not the same as in the USA (MM/dd/yyyy).
DateTime date = new DateTime(2023, 12, 31, 23, 59, 0);
// Standard formats
Console.WriteLine(date.ToString("d")); // Short date (31/12/2023)
Console.WriteLine(date.ToString("t")); // Short time (23:59)
Console.WriteLine(date.ToString("O")); // ISO 8601 (Universal format for JSON/APIs)
// Custom formats
Console.WriteLine(date.ToString("yyyy-MM-dd HH:mm")); // 2023-12-31 23:59
Parsing (Text to Date)
The reverse path is delicate. Converting text to date can fail if the format is not as expected.
string text = "31/12/2023";
// Unsafe way (Throws exception if it fails)
DateTime date1 = DateTime.Parse(text);
// Safe way (TryParse pattern)
if (DateTime.TryParse(text, out DateTime safeDate))
{
Console.WriteLine("Valid date: " + safeDate);
}
else
{
Console.WriteLine("The text is not a valid date");
}
