C# Dates, Times and Time Zones

Choose DateTimeOffset, DateOnly and TimeOnly in C#, parse explicit date formats, handle time zones, and distinguish elapsed time from calendar rules.

Lesson 27 of 43Intermediate10 min read

An appointment date, a shop's opening time, and a payment timestamp describe different things. The appointment may be a calendar date without a clock time. The opening time repeats on many dates. The payment occurred at one instant that can be displayed differently around the world. Putting all three into an unqualified string or a local DateTime leaves important meaning unstated.

This lesson uses .NET 8 and C# 12. DateOnly and TimeOnly are available from .NET 6. You will separate these meanings, parse a stable input format, calculate a duration, and recognize where a regional time-zone rule is necessary. Examples use fixed values so their output does not depend on the current date.

Choose a type from the meaning

DateOnly represents a calendar date without a time or offset. It is suitable for a birthday, invoice date, or holiday rule. TimeOnly represents a time of day without identifying a particular day. It can describe a daily opening time, but cannot by itself identify an instant. TimeSpan represents a duration or interval, such as a thirty-minute appointment length.

DateTimeOffset pairs a date and time with an offset from UTC. That combination identifies an instant, making it a useful choice for events and audit timestamps. The offset is not a named time zone. An offset of two hours does not tell you which region's daylight-saving or historical rules apply. DateTime also carries a Kind value, which distinguishes UTC, local, and unspecified interpretations. An unspecified value needs additional context before conversion to a particular instant is meaningful.

An instant and its display can differ

Two DateTimeOffset values can show different clock readings yet represent the same instant. UTC provides a convenient common representation for comparison and storage, while an application can display the instant with the appropriate local offset. Do not compare formatted clock strings to determine which event occurred first. Compare the represented values instead.

A time zone adds regional rules. TimeZoneInfo can convert an instant into a zone's local clock time. Zone identifiers and installed data can differ by operating system, so configuration should name a supported identifier and handle unavailable zones. The example uses a fixed offset rather than a regional identifier to keep its output portable. A production scheduling feature should retain the named zone when future dates must follow the region's rules, rather than permanently reusing today's offset.

Parse the date contract explicitly

An input such as 03/04/2028 is ambiguous without a culture or a defined format. For a machine-facing contract, an ISO-style date such as yyyy-MM-dd avoids the month/day ambiguity. TryParseExact checks the expected structure without throwing for ordinary invalid input. It also rejects impossible calendar dates, so a well-shaped string is not automatically accepted.

A timestamp contract should state whether it requires an offset or UTC marker. The round-trip O format is useful for preserving the information carried by a date-time value. A birthday should generally remain a date, rather than being converted to midnight UTC and then shifted to a neighboring day when displayed in another zone. Parsing is the beginning of validation: a correctly parsed date may still be too far in the past or outside the permitted booking window.

Calendar arithmetic and elapsed arithmetic differ

Adding one calendar day can mean returning at the same local clock time tomorrow. Adding twenty-four elapsed hours means moving by a fixed duration along the timeline. Near a daylight-saving transition, those requirements can produce different local times. Decide which rule the application promises before implementing its arithmetic.

A local clock reading in a regional zone can be invalid when clocks jump forward. It can be ambiguous when clocks move backward and the same reading occurs twice. Use TimeZoneInfo.IsInvalidTime and IsAmbiguousTime when interpreting local appointment input, then define whether to reject, ask the user, or apply a documented selection policy. Do not silently assume that every local day has exactly twenty-four hours. Tests should include transition dates for the selected zone and should not assume identical zone databases on every system.

Test clock-dependent behavior deliberately

If a service uses the current time to determine expiration, make its clock replaceable. .NET 8's TimeProvider provides an abstraction for retrieving time and for several time-based operations. A fixed or fake provider lets tests place the application exactly before or after a deadline without waiting. Always test whether the endpoint is inclusive: “expires at noon” needs a rule for an event stamped exactly at noon.

For performance timing, prefer a monotonic elapsed-time mechanism such as Stopwatch. Wall-clock timestamps can change when a clock is synchronized or manually adjusted. Store event timestamps for audit and use the appropriate elapsed timer for measurements. Keep formatting at the presentation boundary so a test can compare values directly rather than relying on a locale-specific display string.

Official references

C# Dates, Times and Time Zones Examples

Each example shows the code, the output it produces and what each part does.

Example 1

The same event in two offset displays

This complete console program parses a calendar date independently from an event timestamp. It displays two representations of the event and measures the interval to a later event.

The conversion changes the displayed clock reading, not the event. Date arithmetic correctly crosses the leap-day boundary. The fixed offset makes the example deterministic, but it does not model a regional daylight-saving transition.

C#
using System;
using System.Globalization;

bool valid = DateOnly.TryParseExact("2028-02-29",
    "yyyy-MM-dd", CultureInfo.InvariantCulture,
    DateTimeStyles.None, out DateOnly appointmentDate);
Console.WriteLine($"Date accepted: {valid}");

var received = new DateTimeOffset(
    2028, 2, 29, 10, 0, 0, TimeSpan.FromHours(2));
var completed = received.AddMinutes(45);

Console.WriteLine(received.ToString(
    "yyyy-MM-dd HH:mm zzz", CultureInfo.InvariantCulture));
Console.WriteLine(received.ToUniversalTime().ToString(
    "yyyy-MM-dd HH:mm zzz", CultureInfo.InvariantCulture));
Console.WriteLine($"Elapsed minutes: {(completed - received).TotalMinutes}");
Console.WriteLine(appointmentDate.AddDays(1).ToString(
    "yyyy-MM-dd", CultureInfo.InvariantCulture));
Output
Date accepted: True
2028-02-29 10:00 +02:00
2028-02-29 08:00 +00:00
Elapsed minutes: 45
2028-03-01

C# Dates, Times and Time Zones in Real Projects

Worked exercise: validate a delivery window

A delivery service accepts dates from March 1 through March 7 inclusive. The task is to parse an invariant date and print either the formatted accepted date or a clear rejection. This exercise uses a calendar rule, so DateOnly avoids unnecessary time-zone conversions. The bounds are fixed to make the example repeatable.

C#
using System;
using System.Globalization;

var first = new DateOnly(2028, 3, 1);
var last = new DateOnly(2028, 3, 7);
foreach (string text in new[] { "2028-03-04", "2028-03-08", "2028-02-30" })
{
    bool parsed = DateOnly.TryParseExact(text, "yyyy-MM-dd",
        CultureInfo.InvariantCulture, DateTimeStyles.None,
        out DateOnly date);

    if (!parsed || date < first || date > last)
    {
        Console.WriteLine("Outside delivery window");
        continue;
    }

    Console.WriteLine("Delivery: " + date.ToString(
        "yyyy-MM-dd", CultureInfo.InvariantCulture));
}
Output
Delivery: 2028-03-04
Outside delivery window
Outside delivery window

The second input is a real date outside the business window. The third is not a real date at all. Both are rejected, but a form could distinguish those messages while keeping the same validation boundary. No implicit current-culture assumption influences the accepted syntax.

C# Dates, Times and Time Zones FAQ

Is DateTimeOffset a complete replacement for a time zone?

No. It records an offset for one represented date and time, which is enough to identify an instant. A zone supplies regional transition rules across many dates. If an appointment should occur at nine each morning in a particular city, retain that zone and the local scheduling rule. Reusing one observed offset for all future dates can move appointments by an hour when seasonal rules change.

Should birthdays be stored in UTC?

A birthday usually describes a calendar date, so DateOnly is a better match. Converting that date into midnight UTC introduces an instant that the original information did not contain. Displaying that instant elsewhere can shift the calendar date. Store the actual meaning and introduce a time zone only when the application needs to schedule a concrete event on that date.

Why did adding a day produce a different elapsed duration?

Calendar rules and timeline durations are different. A regional clock can skip or repeat an hour during a daylight-saving transition. Keeping the same local time on the following calendar date may span twenty-three or twenty-five hours. Describe the requirement in words first, then choose the operation and zone conversion that implement it. Test the transition behavior explicitly.

How should I handle ambiguous appointment times?

Detect ambiguity using the selected zone's rules and use a product-defined policy. Asking which offset the user intends is often clearer than silently choosing. An API can require an offset together with the local date-time or reject ambiguous input with a validation response. Document the policy and test both candidate instants. An exception caught without a clear response does not resolve the ambiguity for the caller.

C# Dates, Times and Time Zones Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

An appointment system receives a local time that occurs twice during a clock change. What should it do?

C# Dates, Times and Time Zones Quiz

2 questions to check what you learned.

  1. Which type best represents a birthday without a clock time?

    • TimeSpan
    • DateOnly
    • TimeOnly
    • A local timestamp converted to UTC
  2. What does an offset of +02:00 tell you about a DateTimeOffset value?

    • The complete daylight-saving rules for its city.
    • That its region never changes offset.
    • The clock reading's offset from UTC at the represented instant.
    • The operating system's local time-zone identifier.