Sitemap

Time

11 min readMay 17, 2025

--

Press enter or click to view image in full size

Yo everyone! In this article, we’ll dive into the greatest mystery of humanity — time. Time in C#.

If you think working with time is easy, you are either lying or doing it wrong🙃. In this article, we’ll explore various concepts of time, the available time-related data structures in C#, common pitfalls when working with time, and practical tips to handle them effectively.

So, let’s not waste any more of it — time to begin.

Concepts

What is time? It’s the clock on the wall that never stops ticking, the days in your calendar, a never-ending sequence of events. One can not simply explain time. Time is a complex abstraction that can be described in multiple concepts that we are about to explore.

✨ Instant — “What moment is right now?”

A moment is something that is happening right now.

However, you can not catch the current timestamp, it will always be in the past. Typically, a moment in time is represented as time that has passed from some starting point.

  • Example: the Unix Epoch is the starting point for time in many systems. It measures time by the number of seconds that have elapsed since January 1st, 1970 at 00:00:00 UTC

It’s a simple number, like 1262304000, representing a specific moment (in this case, 1 January 2010).

C# does not have a specific type for a moment, but you can convert a number to a time:

DateTimeOffset epoch = DateTimeOffset.FromUnixTimeSeconds(1_712_831_400);

Storing time as a single number makes it easy to compare, store, sort, and calculate durations.

It is often used in databases, APIs, logs, etc.

🗓 Date — “Which day?”

Often, people refer to time as a calendar day — year, month, and day.

  • Example: 2025-04-11

In C#, you would use DateOnly if you don’t care what time of day it is — just the day.

DateOnly someDate = new DateOnly(year: 2025, month: 4, day: 11);

It is often used for birthdays, deadlines, holidays.

⏰ Time — “What time of day?”

Represents a time of day — hours, minutes, seconds — without a date.

  • Example: 14:30:00 (2:30 PM)

In C#, use TimeOnly to represent this:

var time = new TimeOnly(hour: 14, minute: 23, second: 30);
Console.WriteLine(time); // 14:23

Used for opening hours, setting an alarm, and daily routines.

🕒 Date and Time — “When exactly?”

Sometimes, you want to have a full timestamp: both date and time.

  • Example: 2025-04-11 14:30:00

DateTime is what you need.

// from higher to lower value
// year - month - day - hour - minute - second
DateTime date = new DateTime(2015, 7, 20, 18, 30, 25);
Console.WriteLine(date); // 20.07.2015 18:30:25

Use cases: timestamps, logs, event scheduling.

⏳ Duration — “How long?”

Occasionally, we’re not talking about when something happened — but how long it took.

  • Example: a duration could be 2 hours or 15 minutes

In C#, this is represented by TimeSpan:

TimeSpan duration = TimeSpan.FromHours(2.5); // 2 hours and 30 minutes
TimeSpan duration = DateTime.Now.AddDays(1) - DateTime.Now; // 24 hours

Useful for timers, countdowns, and measuring how long something took.

🔄 Schedule — “When should something repeat?”

A tiny schedule that tells a system when to run something can be represented with a cron expression.

  • Example: “run this every Monday at 9 AM” or “every 5 minutes”

It is not a date or time itself, more like a pattern.

The cron expression is made of five fields. Each field can have the following values.

{minute} {hour} {day} {month} {day of the week}

* * * * * // every minute
0 * * * * // every hour
0 */5 * * // every five minutes
0 0 * * * // every day at midnight
0 9 * * 1 // at 9:00 AM every Monday

There are other formats. For example, Azure functions rely on NCrontab, which is similar but with 6 parameters.

Used to trigger background jobs, data processing, a repeating schedule.

📅 Calendar — “How do we count days?”

A calendar is a system for organizing dates — like how we humans agree on what day it is.

For example:

  • What day is after February 28? (It depends — maybe March 1, maybe Feb 29 in a leap year)
  • What’s the first day of the week? (Sunday in the U.S., Monday in Europe)
  • Should the day begin at midnight or at sunset?

In C#, calendars are part of System.Globalization, and you can use built-in ones like this:

new DateOnly(2022, 1, 6, new GregorianCalendar()); // 06.01.2022 - Thursday
new DateOnly(2022, 1, 6, new JulianCalendar()); // 19.01.2022 - Wednesday
new DateOnly(2022, 1, 6, new JapaneseCalendar()); // 06.01.4040 - Friday

Not all cultures use the same calendar. If you’re building apps for people around the world, your app should know which calendar to use.

🧭 Time Zone — “Where in the world?”

The world is split into regions that follow different clocks.

For example, New York (08:00/8:00 AM) and London (13:00/1:00 PM) have different times, even if it’s the same moment.

Press enter or click to view image in full size

Everything begins with UTC, the global reference time that remains constant regardless of time zones and is unaffected by daylight saving. All other time zones are defined as minute offsets from UTC.

For instance, London operates at UTC+1, while New York is at UTC-4, resulting in a 5-hour time difference between the two cities.

Sometimes, the offset can even be 30 or 45 minutes. It can be different for the same city in the summer and winter.

In C#, time zone information is represented by TimeZoneInfo class:

ReadOnlyCollection<TimeZoneInfo> allTimeZones = TimeZoneInfo.GetSystemTimeZones();

TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById("Tokyo Standard Time");
Console.WriteLine(timeZone); // (UTC+09:00) Osaka, Sapporo, Tokyo

When working with people or systems in different locations, you need to be clear about which time zone you’re talking about.

🌐 DateTimeOffset — “What time is it and where?”

DateTimeOffset represents a date and time with a time zone offset. It’s like DateTime, but smarter about where in the world the time is from.

  • Example: April 11, 2025 at 14:30 (UTC+02:00)

It helps avoid bugs when working across time zones — like syncing calendars or sending reminders globally.

DateTimeOffset now = DateTimeOffset.Now; // 11.04.2025 17:50:35 +03:00

Use this when you want to store or compare exact global moments but still know the original local time.

Problems with Time

Working with time presents multiple problems. While it seems straightforward at first — just store the current time, right? — the complexity quickly escalates when real-world scenarios come into play.

Most of the problems are caused by not knowing which data structure to use, when to apply a time zone offset, and how to display time.

Let’s see.

DateTime

Even though everybody likes good old DateTime, you should not use it in your code 🙃.

The main problem with DateTime is that, despite its ability to represent both UTC and local time, it doesn't store time zone information.

Let’s say we have April 11, 2025 at 14:30.

Here, we don’t know if it is in UTC or local. DateTime has Kind property indicating whether the time is based on local time, UTC or neither. But that's it. You do not have information about the local time where the data originated.

// 11.04.2025 14:30:00 DateTimeKind.Local
var local = new DateTime(2025, 04, 11, 14, 30, 00, DateTimeKind.Local);

Imagine we want to convert local time back to UTC. DateTime will apply server timezone, leading to different results depending on which machine the code is running:

// 11.04.2025 ❓:30:00 DateTimeKind.Utc
var utc = local.ToUniversalTime();

Look, those two dates represent the same moment in time; however, they are not even considered equal:

utc == local // false

A similar problem happens with DateTime.Now. It implicitly applies the server's timezone and will yield different results.

// your machine
DateTime.Now; // 14.04.2025 14:30:00 DateTimeKind.Local

// machine in the cloud
DateTime.Now; // 14.04.2025 12:30:00 DateTimeKind.Local

Making it impossible to store it in a database.

Get iamprovidence’s stories in your inbox

Join Medium for free to get updates from this writer.

To make DateTime work, the team should agree on the next rules:

  • use only UTC in code
  • do not use DateTime.Now
  • you can also set the server time zone to UTC. In this case, even if some places still use DateTime.Now it will behave like DateTime.UtcNow
  • when you need to show the user his local time, perform explicit conversion. Get the user’s timezone from a database, but not TimeZoneInfo.Local

DateTimeOffset

DateTimeOffset is a better option since it stores information about the time zone offset.

// 11.04.2025 14:30:00 +03:00
var local = new DateTimeOffset(2025, 04, 11, 14, 30, 00, TimeSpan.FromHours(3));

Converting it back to UTC will always be the same:

// 11.04.2025 11:30:00 +00:00
var utc = local.ToUniversalTime();

In this case, it is the same moment in time:

utc == local // true

Overall, DateTimeOffset is a more complete data structure that stores full information about time, unambiguous due to explicit offset, safer for serialization, and more accurate during comparisons.

UTC

Let’s say we have a blogging application. A user in New York at 10:30 has created a post. This post is sent to the API server that is in London, where the time is 15:30. The question is: which time should we show to a user who is in Ukraine at 17:30?

Press enter or click to view image in full size

Technically, all that happened at the same moment, 14:30 UTC. And it is obvious that we should display the date according to the user’s local time.

To achieve that, you should:

  • when a user creates something in the API, dates should be in a format that includes the time zone’s offset. Usually, ISO 8601 format is used for that:
// from higher to lower value
// YYYY-MM-DDTHH:mm:ssZ

2025-04-11T10:30:00+01:00 // local
2025-04-11T14:30:00Z // UTC, Zulu time, Zero time

(the client can also just send data in UTC)

  • the server should be indifferent to its local time zone and always store the date in UTC
  • when another user retrieves the data, the client app can easily convert it to a local time since it knows the dates are always in UTC

Overall, dates should be stored in UTC and displayed in the local time zone.

In case your server needs to display data in the user’s time (e.g., you generate reports), it should apply the time zone offset to displayed dates. This can usually be achieved by storing the user’s time zone in the database.

DateTime utcNow = DateTime.UtcNow;

TimeZoneInfo targetTimeZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
DateTime targetLocalTime = TimeZoneInfo.ConvertTimeFromUtc(utcNow, targetTimeZone);

(Alternatively, you can store dates in time-zone-aware data types like DateTimeOffset)

Culture

When you display a date, it is important not only to apply the time zone offset but also to specify the culture.

In the code below, the date format depends on the culture of the environment:

DateTime now = new DateTime(2025, 4, 14);
now.ToString(); // Output depends on system culture

If the app runs on a machine with en-US culture, you might get:

4/14/2025 12:00:00 AM

But on fr-FR:

14/04/2025 00:00:00

Users in different regions might see incorrect formats and get confused.

Always specify the culture when converting dates (or numbers) to a string:

DateTime now = new DateTime(2025, 4, 14);
now.ToString(new CultureInfo("en-US")); // consistent output

This way, the behaviour does not vary when the code runs on a different machine (for example, in unit tests on CI/CD server).

Local time in Business Logic

Your business logic on the server may depend on the local time of a user or tenant:

At midnight generate reports for previous day

In that case, you must calculate local time and perform all time-based calculations accordingly:

var nextMidnight = GetLocalMidnight(tenant.TimeZone).AddDays(1);

tenant.PlannedGenerateReportTime = nextMidnight;

. . .

static DateTimeOffset GetLocalMidnight(TimeZoneInfo tz)
{
var utcNow = DateTimeOffset.UtcNow;
var localNow = TimeZoneInfo.ConvertTime(utcNow, tz);
var localMidnight = new DateTimeOffset(localNow.Date, tz.GetUtcOffset(localNow));

return localMidnight.ToUniversalTime();
}

Always ensure that your business logic is aware of and respects the users’ local time zone.

Date without time

Sometimes, we only deal with the date part without a time component. For example:

An employee has a day off on April 11, 2022

If we were to apply someone's time zone in UTC-11 to this date, the “April 11, 00:00:00” would be converted to “April 10, 23:00:00”. This can lead to confusion.

Therefore, the “pure” dates must not be converted between time zones.

Future Dates

While storing time in UTC sounds good, it does not always work. This is the case for the future dates.

Let’s say two users scheduled a meeting at 12:00 local time. You store it in UTC, which would be 10:00.

Then one country decided to shift to a different time zone, apply Daylight Saving Time settings, or simply change UTC offsets for some unknown, stupid political reason 😒.

If you restore UTC to local, it will mean one user will miss the meeting, since the future UTC that you calculated now can be different at that moment in the future.

Press enter or click to view image in full size

The simplest way to solve this problem is by preserving the local timestamp with an offset for future appointments, calendar events, and things that must align with people’s expectations in their local time. You can fund other solutions here.

Mock DateTime in unit tests

Imagine you have a service that calculates when a reminder should be sent:

class ReminderCalculator
{
public DateTimeOffset Calculate(DateTimeOffset meetingTime)
{
var now = DateTimeOffset.UtcNow; // 👈
var timeUntilMeeting = meetingTime - now;

if (timeUntilMeeting < TimeSpan.FromHours(24))
{
return meetingTime.AddMinutes(-30);
}

return meetingTime.AddDays(-1);
}
}

The problem here is the usage of DateTimeOffset.UtcNow. It is a static class that cannot be mocked in unit tests.

Developers address this issue by introducing the next class:

class ITimeService
{
DateTimeOffset GetUtcNow();
}

class TimeService : ITimeService
{
DateTimeOffset GetUtcNow() => DateTimeOffset.UtcNow;
}

It allows for easy mocking of an uncontrolled dependency.

By the way, Microsoft recently added an abstract TimeProvider class that can do the same:

class ReminderCalculator(TimeProvider _timeProvider)
{
public DateTimeOffset Calculate(DateTimeOffset meetingTime)
{
var now = _timeProvider.GetUtcNow(); // 👈
var timeUntilMeeting = meetingTime - now;

if (timeUntilMeeting < TimeSpan.FromHours(24))
{
return meetingTime.AddMinutes(-30);
}

return meetingTime.AddDays(-1);
}
}

. . .
// do not forget to register it in the DI
services.AddSingleton(TimeProvider.System);

In case your class is out of DI context (e.g., rich entity or static class), the date can be passed as input parameters:

class Note
{
public int Id { get; private set; }
public string Text { get; private set; }
public DateTime UpdatedAt { get; private set; }

public void ChangeText(
string newText,
DateTime updatedAtUtc) // 👈
{
if (updatedAtUtc.Kind != DateTimeKind.Utc)
{
throw new ArgumentException("The date must be in UTC.", nameof(updatedAtUtc));
}

this.Text = newText;
this.UpdatedAt = updatedAtUtc;
}
}

Without injecting a clock or using test doubles, your tests will be fragile and non-deterministic.

General recommendations

Working with time in C# is more complex than it appears. Developers often face issues related to time zones, ambiguous DateTime representations and inconsistencies across different machines.

Follow general recommendations to avoid the most notorious bugs:

  • do not use DateTime.Now
  • favor DateTimeOffset over DateTime
  • serialize date in ISO 8601 format in your API
  • store dates in UTC, display in local
  • specify the culture when converting a date to a string
  • configure all servers/databases to be in UTC
  • respect the user’s local time in your business logic
  • use local time for future events that should adapt to time zone changes
  • the “pure” dates must not be converted between time zones
  • make sure that the current time can be mocked in unit tests

Let me know in the comment section how often you struggle with time 💬 Clap if you like the article👋 Support me with a coffee ☕ Follow to receive more ✅ And don’t watch the clock; just enjoy the moment! 😜

--

--

iamprovidence
iamprovidence