← Open in the full interactive course (progress tracking, search & more)

A date without a time zone is a sentence with no context — it sounds complete, but it doesn't actually tell you when something happened.

A booking system records that an appointment was made at 2026-08-29 14:30. Sounds precise. But 2:30 PM where? If the server that saved it was in New York and the customer who reads it back is in Tokyo, "2:30 PM" means two completely different moments in absolute time — and neither the number nor the code that stored it says which one was meant.

This ambiguity is one of the most common, quietly damaging bug sources in real software — appointments that appear to shift by hours, reports that seem to double-count a day, logs that look like they happened in the wrong order. In this lesson, you'll learn the difference between DateTime, DateTimeOffset, and the newer DateOnly/TimeOnly types — and exactly why naive use of DateTime causes real, hard-to-track bugs.

What Is It?

The Simple Explanation

.NET gives you several types for representing moments and durations of time, and choosing the right one matters:

The Technical Definition

System.DateTime stores a date and time value along with a Kind property (Utc, Local, or Unspecified) — but critically, Kind is just a label; it doesn't actually convert or validate anything, and it defaults to Unspecified in many common construction paths. System.DateTimeOffset instead stores the date, the time, and a precise offset from UTC (e.g. +05:30) as an intrinsic part of the value itself — so two DateTimeOffset values can always be compared correctly regardless of what offset each one happens to be expressed in. DateOnly and TimeOnly, added in more recent .NET versions, deliberately strip away the part of the value you don't need, so the type itself documents your intent.

TypeRepresentsTime zone aware?Good for…
DateTimeDate + timeOnly via the Kind label — easy to get wrongLegacy code, or when you fully control both ends and are careful
DateTimeOffsetDate + time + UTC offsetYes — the offset is part of the valueTimestamps, "when did this event actually happen" — the modern default
DateOnlyJust a calendar dateN/A — no time componentBirthdays, due dates, "which day," not "which moment"
TimeOnlyJust a time of dayN/A — no date componentStore hours, a recurring daily schedule

Why Does It Exist?

The Problem — DateTime.Kind Is a Suggestion, Not a Guarantee

A DateTime's Kind can be Utc, Local, or Unspecified — but this is just metadata attached to the value. Nothing stops two different parts of a codebase from creating DateTime values with different, unstated assumptions about which zone they're in, and then comparing or combining them as if they matched.

DateTime savedAt = DateTime.Now; // Kind = Local — but "local" to which server, which user? DateTime deadline = new DateTime(2026, 9, 1); // Kind = Unspecified — nobody actually knows what zone this is if (savedAt > deadline) // comparing values that might not even be in the same zone { // This comparison can be silently wrong depending on where the code runs }

Worse, this bug is invisible in the code itself — it compiles, it runs, it often even looks correct in testing on a single developer's machine, because that machine's local time zone happens to match every assumption baked into the code. It only surfaces later — a server deployed in a different region, a user in a different time zone, daylight saving time shifting the offset partway through the year.

The Need

Applications need a way to represent a moment in time that is unambiguous — comparable and combinable correctly no matter where the code creating it, or the code reading it, happens to be running.

The Solution — DateTimeOffset (and UTC storage)

DateTimeOffset makes the time zone information an unavoidable, built-in part of the value itself — there's no separate "Kind" flag that can be forgotten or mismatched. Combined with the standard practice of always storing timestamps in UTC (covered fully in the next lesson on time zones), this removes the ambiguity at its source instead of trying to carefully track it by convention.

Naive DateTime

DateTimeOffset

Big Picture

Picking the right type is really about asking: what am I actually trying to represent?

WHICH TYPE DO I NEED?
"When exactly did this event happen, unambiguously?"
    ↓
DateTimeOffset (order placed, payment processed, log entry)

"What calendar date, regardless of time or zone?"
    ↓
DateOnly (birthday, invoice due date, holiday)

"What time of day, regardless of which day?"
    ↓
TimeOnly (store opens at, alarm goes off at)

How It Works

CREATING AND USING THE TYPES
Step 1 — Get "now" the unambiguous way
DateTimeOffset now = DateTimeOffset.UtcNow;
// e.g. 2026-08-29T18:42:11.0000000+00:00 — always UTC, always unambiguous
Step 2 — Extract a date-only or time-only value when that's really what you mean
DateOnly dueDate = new DateOnly(2026, 9, 15);
TimeOnly storeOpens = new TimeOnly(9, 0); // 9:00 AM

// Also works directly from an existing DateTime/DateTimeOffset:
DateOnly today = DateOnly.FromDateTime(DateTime.Now);
Step 3 — Compare and do arithmetic with confidence
DateTimeOffset orderPlaced = DateTimeOffset.UtcNow;
DateTimeOffset expiresAt = orderPlaced.AddMinutes(15);

if (DateTimeOffset.UtcNow > expiresAt)
    Console.WriteLine("This order confirmation has expired.");

Simple Example

// ─── The naive way — ambiguous ─── DateTime meetingTimeNaive = new DateTime(2026, 9, 3, 14, 0, 0); Console.WriteLine(meetingTimeNaive.Kind); // Unspecified — 2 PM... where? // ─── The unambiguous way ─── DateTimeOffset meetingTime = new DateTimeOffset(2026, 9, 3, 14, 0, 0, TimeSpan.FromHours(-5)); // 2 PM in UTC-5 Console.WriteLine(meetingTime); // 2026-09-03 14:00:00 -05:00 Console.WriteLine(meetingTime.UtcDateTime); // 2026-09-03 19:00:00 — the same moment, in UTC // ─── A date without a time — a due date ─── DateOnly invoiceDue = new DateOnly(2026, 9, 30); Console.WriteLine(invoiceDue); // 09/30/2026 — no time, no ambiguity about "which moment" // ─── A time without a date — recurring store hours ─── TimeOnly closingTime = new TimeOnly(21, 0); // 9:00 PM, every day Console.WriteLine(closingTime); // 9:00 PM

Notice meetingTime.UtcDateTime — because the DateTimeOffset carries its -05:00 offset as part of the value, .NET can reliably convert it to the equivalent UTC moment at any time, from anywhere. A plain DateTime with Kind = Unspecified gives you no such guarantee — there's nothing to convert from.

Real-World Example

A booking system that records when an appointment was created, alongside the calendar date and time slot the customer actually booked (which are intentionally date/time-only, since they represent a wall-clock slot, not a single global instant until combined with the customer's zone):

public class Appointment { public int Id { get; init; } public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow; // when the booking was made — unambiguous public DateOnly AppointmentDate { get; init; } // which calendar day public TimeOnly AppointmentTime { get; init; } // which time slot on that day public string CustomerTimeZoneId { get; init; } = "America/New_York"; // needed to know what the slot actually means globally } // ─── Usage ─── var appointment = new Appointment { Id = 1001, AppointmentDate = new DateOnly(2026, 9, 10), AppointmentTime = new TimeOnly(14, 30), // 2:30 PM, in the customer's own time zone CustomerTimeZoneId = "America/New_York" }; Console.WriteLine($"Booked {appointment.CreatedAt:u}"); // an unambiguous, sortable global timestamp Console.WriteLine($"Appointment: {appointment.AppointmentDate} at {appointment.AppointmentTime} ({appointment.CustomerTimeZoneId})");

This is a genuinely realistic modeling choice: CreatedAt is a true global instant, so it's a DateTimeOffset. But the appointment slot itself is really "2:30 PM on September 10th, in the customer's own local time" — a wall-clock concept, which is exactly what DateOnly and TimeOnly were introduced to represent cleanly, alongside the time zone identifier needed to interpret it globally (full time zone handling is the subject of the next lesson).

Analogy

A Flight Time Without an Airport Code

Saying "the flight departs at 9:00 AM" is useless on its own — 9:00 AM in New York, or 9:00 AM in Tokyo? A boarding pass always pairs the time with the airport (and implicitly, its time zone) — JFK 09:00 is unambiguous in a way that "9:00 AM" alone never can be.

A plain DateTime is the time without the airport code. A DateTimeOffset is the boarding pass — the moment and its zone travel together as one inseparable piece of information, so it means the same thing no matter who reads it or where they're reading it from.

Common Confusion

1. "DateTime.Kind = Utc means it's automatically correct"

Setting Kind to Utc is just labeling the value — it doesn't verify anything or convert anything. DateTime.SpecifyKind(someLocalValue, DateTimeKind.Utc) will happily produce a DateTime claiming to be UTC that is, in fact, still a local time value underneath — the label lied, and nothing in the type system catches that. DateTimeOffset avoids this entire category of mistake because the offset isn't a detachable label; it's baked into the value at construction.

2. DateTimeOffset vs. time zone — they're related but not the same thing

A DateTimeOffset stores a fixed numeric offset from UTC (like -05:00) at the moment it was created — it does not store a named time zone (like "Eastern Time") or know anything about that zone's daylight saving rules. Two different named zones can share the same offset at a given moment, and the same named zone's offset can change across the year (daylight saving). Working with actual named zones and their rules is the subject of the next lesson, TimeZoneInfo.

Common Mistakes

Mistake 1 — Using DateTime.Now for timestamps that get compared or stored

Wrong:

public DateTime CreatedAt { get; set; } = DateTime.Now; // "local" to whichever machine runs this

Correct:

public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow; // unambiguous, everywhere

Mistake 2 — Using DateTime for a value that's really just a date

Storing a birthday as DateTime — it silently carries a meaningless time component (usually midnight), which can then be accidentally affected by time zone conversions that have no business touching a birthday at all. Use DateOnly, which structurally cannot carry a time component to get confused about.

Mistake 3 — Comparing DateTime values from different sources without checking Kind

Comparing a DateTime that came from a database (often Unspecified) against DateTime.Now (Local) without ever confirming they're actually in the same zone. This comparison can compile and "work" during development purely by coincidence, and then be wrong the moment the assumption doesn't hold. Prefer DateTimeOffset so this ambiguity can't arise in the first place.

When Should I Use It?

Recording "when did this happen"
DateTimeOffset (usually via UtcNow) — order timestamps, log entries, audit trails.
A calendar date, no time
DateOnly — birthdays, due dates, holidays.
A time of day, no date
TimeOnly — store hours, recurring daily schedules.
Legacy or interop code
Plain DateTime still shows up in older APIs — handle its Kind deliberately when you must use it.
Rule of thumb: If you're recording a specific moment something happened, reach for DateTimeOffset (and UTC) by default. If you're really only representing a calendar date or a time of day — not a global instant — DateOnly/TimeOnly say exactly that, with no room for the ambiguity plain DateTime invites.

Mental Model

DateTime = a time with a label that might be lying.
DateTimeOffset = a time with its zone welded on — can't be lost or forgotten.
DateOnly = "which day," deliberately with no notion of "which moment."
TimeOnly = "what time of day," deliberately with no notion of "which day."

Remember:
· DateTime.Kind is metadata you can set incorrectly — it doesn't validate anything.
· DateTimeOffset makes ambiguity structurally impossible, not just conventionally avoided.
· Use DateOnly/TimeOnly when a value genuinely isn't a specific global moment.

Key Takeaway


Check Your Understanding

You've seen why plain DateTime is ambiguous and how DateTimeOffset, DateOnly, and TimeOnly each solve a specific piece of that problem. Let's check your understanding.

1. Why is a plain DateTime value considered ambiguous even when its Kind property is set?

Show answer

Correct: B

Why B is correct: Kind is metadata, not a verified guarantee. You can mark a DateTime as Utc via SpecifyKind even if the underlying value is actually a local time — nothing checks that assertion. This means two DateTime values from different parts of a codebase can carry mismatched, unverified assumptions about their zone.

Why A is incorrect: That limitation applies to some older Unix timestamp formats, not to .NET's DateTime, which supports a much wider range of years.

Why C is incorrect: DateTime values can be compared — the problem is that the comparison can be silently wrong if the two values are actually in different zones despite looking comparable.

Why D is incorrect: DateTime has no hard-coded time zone — its meaning depends entirely on the (unverified) Kind label and how it was constructed.

Reinforcement: The core issue is that Kind is an unverified label, not an intrinsic, trustworthy part of the value the way DateTimeOffset's offset is.

2. What makes DateTimeOffset structurally different from DateTime in a way that prevents the ambiguity problem?

Show answer

Correct: A

Why A is correct: Unlike DateTime's detachable Kind label, a DateTimeOffset's numeric UTC offset is baked into the value at construction and travels with it everywhere — there's no way to "forget" it or attach the wrong one after the fact the way you can with SpecifyKind.

Why B is incorrect: DateTimeOffset can be constructed many ways — with an explicit date, time, and offset, from a DateTime, or via UtcNow/Now — it isn't limited to just one factory method.

Why C is incorrect: DateTimeOffset stores a numeric offset (like -05:00), not a named zone like "America/New_York" — named zones and their rules are TimeZoneInfo's job, covered next.

Why D is incorrect: DateTimeOffset stores its data numerically/structurally, the same general approach as DateTime — the difference is what additional information is stored, not the underlying representation format.

Reinforcement: The offset being an inseparable part of the value — not an optional, unverified label — is exactly what eliminates the ambiguity.

3. You need to store a customer's date of birth. Which type is the best fit, and why?

Show answer

Correct: B

Why B is correct: A birthday is fundamentally just a calendar date — it doesn't represent a specific global instant, and it has no meaningful time-of-day component. DateOnly represents exactly that, with no room for an unwanted time component to introduce confusion.

Why A is incorrect: A birthday isn't a specific global moment in time that needs an offset — attaching one would be meaningless and could even introduce the exact ambiguity this lesson warns against.

Why C is incorrect: TimeOnly represents a time of day with no date at all — a birthday needs the date (year, month, day), which TimeOnly doesn't capture.

Why D is incorrect: This reintroduces exactly the ambiguity problem the lesson describes — a meaningless midnight timestamp that could be mishandled by time zone conversions that have no business affecting a birthday.

Reinforcement: Choose the type that matches what you're actually representing — DateOnly for "which day," not "which moment."

4. A junior developer writes: DateTime.SpecifyKind(someDateTime, DateTimeKind.Utc) on a DateTime value that was actually captured as local time, assuming this "converts" it to UTC. What actually happens?

Show answer

Correct: B

Why B is correct: SpecifyKind only changes the label — it performs no actual time conversion or validation. If the original value was really a local time, it remains that same local time numerically, just now incorrectly claiming to be UTC. This is exactly the "label that might be lying" problem from the lesson.

Why A is incorrect: Actual conversion (adjusting the numeric value to reflect a different zone) is what ToUniversalTime() does, not SpecifyKind — SpecifyKind deliberately does not touch the underlying value.

Why C is incorrect: This code compiles and runs without error — the danger is precisely that it looks successful while silently mislabeling the value.

Why D is incorrect: The Kind property genuinely does change — that's the whole (deceptively simple-looking) point of the method; the danger is that the underlying value doesn't change along with it.

Reinforcement: SpecifyKind relabels without converting — this is exactly the kind of unverified assumption that makes plain DateTime risky to rely on.

5. Why is it recommended to use DateTimeOffset.UtcNow (rather than DateTime.Now) when recording the timestamp of an event like an order being placed?

Show answer

Correct: B

Why B is correct: DateTimeOffset.UtcNow captures the current instant with an explicit, fixed UTC offset — so it means the same thing and compares correctly no matter what machine or time zone reads it later. DateTime.Now reflects whatever local zone the executing machine happens to be set to, which can vary between servers, deployments, or over time due to daylight saving.

Why A is incorrect: Performance isn't the reason for this recommendation — both are fast; the concern is correctness and unambiguity.

Why C is incorrect: DateTime.Now works on every OS .NET supports — its behavior isn't restricted to Windows.

Why D is incorrect: DateTime.Now still compiles and runs perfectly fine — it's a valid API, just one that's easy to misuse for timestamps that need to be unambiguous across machines.

Reinforcement: Recording "when did this happen" as an unambiguous global instant is exactly what DateTimeOffset.UtcNow is designed for.

You now understand why UTC-anchored, unambiguous timestamps matter. Next, we build on this directly: TimeZoneInfo, converting between zones for display, and the daylight saving traps that catch even experienced developers off guard.


dotnetmadeeasy.com — Learn C# and .NET, the right way.