Osy#betaRun it on Osyrin: osyrin.com

Reference / Types

DateTimeOffset

DateTimeOffset RecordedAt;

An instant *plus the offset it was recorded at*. Use it when the local time something happened is part of the fact — a warehouse `TIMESTAMP_TZ`, a receipt, an event log that spans zones. When you only need *when*, use `DateTime`: every one of those is already a UTC instant.

stable2 examples compiled by CItypesdatesauthoring

Summary#

A DateTimeOffset is an instant and the offset it was recorded at. That second half is the whole reason to reach for it: a DateTime here is always a UTC instant, so an invoice stamped 09:00+05:30 and one stamped 03:30Z are the same DateTime — and only the first tells you it was morning in India.

DateTimeOffset recordedAt = order.PlacedAt;   // 2026-09-14T14:30:00+05:30

recordedAt.Hour        // 14 — the clock on the wall where it happened
recordedAt.Offset      // +05:30, as a TimeSpan
recordedAt.UtcDateTime // 09:00 — the same moment, as an ordinary DateTime

Signature#

DateTimeOffset At;                 // on an entity or a class
DateTimeOffset Stamp(DateTimeOffset at) { … }

DateTimeOffset.UtcNow              // now, at offset zero
DateTimeOffset.FromUnixTimeSeconds(s)   // an instant sent as seconds since 1970, at offset zero
DateTimeOffset.Parse("2026-09-27T12:30:00+02:00")   // ISO text; text with no offset is read as UTC
new DateTimeOffset(someDateTime)        // the same instant, at offset zero
at.ToUniversalTime()                    // the same instant, at offset zero (a DateTime answers itself)
at.ToUnixTimeMilliseconds()             // …and back, as milliseconds
at.ToOffset(TimeSpan.FromHours(1))      // the same instant, read at another offset

Description#

The parts are the LOCAL reading#

.Year, .Month, .Day, .Hour, .Minute, .Second and .DayOfWeek all answer about the wall clock the value carries — the time where it was recorded. That is the information the offset exists to preserve, so reading them as UTC would quietly be about a different moment of the day.

Two members have no counterpart on a DateTime:

.Offsetthe offset itself, as a TimeSpan (TimeSpan (durations))
.UtcDateTimethe same instant as a plain DateTime, offset discarded
.DateTimethe local reading as a plain DateTime, offset discarded

Comparison is by the instant#

<, > and == compare the moment, never the local reading. Two values recorded thirteen and a half hours apart on the clock can be equal, because they name the same instant:

// 14:30 in India and 01:00 in Los Angeles are both 09:00Z.
indiaStamp == losAngelesStamp   // true

Sorting a column of them orders by instant too, which is almost always what a timeline wants.

Converting#

A DateTime widens to a DateTimeOffset on its own, because nothing is lost — it lands at offset zero and says so. There is no conversion back, deliberately: dropping an offset is a choice about which instant you meant, so you name it.

DateTimeOffset stamped = someDateTime;   // ✓ implicit — lands at +00:00
DateTime plain = stamped.UtcDateTime;    // ✓ the instant
DateTime local = stamped.DateTime;       // ✓ the wall clock it was recorded at

new DateTimeOffset(dateTime) says the widening out loud, and ToUniversalTime() moves a DateTimeOffset to offset zero (on a DateTime, which is already a UTC instant here, it answers the value itself). DateTimeOffset.Parse reads ISO-8601 text and keeps the offset the text carries; text with NO offset is read as UTC — never as the server's local zone, which would make the answer depend on where it runs.

Arithmetic, and mixing with a DateTime#

DateTime and DateTimeOffset are one family, as in C#. Wherever one of each meets an operator, the DateTime widens (to offset zero) and the DateTimeOffset operator applies — so there is nothing to convert by hand:

writtenanswers
dto - dto, dateTime - dto, dto - dateTimea TimeSpan — the time between the two instants
dto + span, dto - spana DateTimeOffset, at the same offset
dateTime < dto, <=, >, >=ordered by the instant
dateTime == dto, !=equal when they name the same instant, whatever the offsets
DateTime due = order.DueAt;                 // a UTC instant
DateTimeOffset seen = order.SeenAt;         // 14:30 +05:30
TimeSpan early = due - seen;                // ✓ widened, then subtracted
DateTimeOffset reminder = seen + TimeSpan.FromHours(1);   // 15:30 +05:30 — the offset is kept
bool late = seen > due;                     // ✓ by the instant

The same rules hold in a server function, in a browser action, and in a query that runs in the database — Reading.Where(r => r.At > cutoff) with cutoff a DateTime, or r.At - r.Logged > TimeSpan.FromHours(1), is answered there exactly as it would be in memory.

Choosing between them meets at DateTimeOffset too. flag ? dateTime : dto, maybeDateTime ?? dto, and a switch whose arms give one of each are all a DateTimeOffset, with the DateTime side converted (to offset zero, a DateTime? that is null staying null) — as C#'s best common type is. The same conversion happens wherever a DateTime is written into a DateTimeOffset slot: a local, an assignment, an argument, a return, a member. In a query's select list the result carries the offset of whichever side the row took.

What C# does not have, Osy# does not either: there is no TimeSpan + DateTimeOffset (the instant goes on the left), and two instants cannot be added.

Unix time, and reading at another offset#

An API that sends an instant as a count of seconds or milliseconds since 1970 is read the way C# reads it, with DateTimeOffset.FromUnixTimeSeconds and DateTimeOffset.FromUnixTimeMilliseconds, and written back with .ToUnixTimeSeconds() and .ToUnixTimeMilliseconds(). ToOffset reads the same instant at another offset, and adding to a DateTimeOffset (AddDays, AddHours, AddTicks, …) keeps its offset:

var shipped = DateTimeOffset.FromUnixTimeSeconds(1616173619);   // 2021-03-19 17:06:59 +00:00
var inParis = shipped.ToOffset(TimeSpan.FromHours(1));          // 2021-03-19 18:06:59 +01:00
var dueBack = inParis.AddDays(14);                              // still +01:00
long sentAt  = dueBack.ToUnixTimeMilliseconds();                // milliseconds since 1970
long seconds = dueBack.ToUnixTimeSeconds();                     // …or seconds, the coarser wire form

⚠ Both directions are long, and only a DateTimeOffset has them. .ToUnixTimeSeconds() and .ToUnixTimeMilliseconds() are declared on this type alone, because the conversion needs the offset to know which instant it is naming — a bare DateTime does not carry one. Convert first if that is what you hold.

The epoch itself is a value you can name rather than a literal you retype: DateTimeOffset.UnixEpoch is 1970-01-01 at offset zero, and DateTime.UnixEpoch is the same moment as a plain instant, for arithmetic that stays in DateTime:

var age    = DateTimeOffset.UtcNow - DateTimeOffset.UnixEpoch;  // a TimeSpan since 1970
var asDate = DateTime.UnixEpoch.AddSeconds(1616173619);         // the same read, in DateTime terms

It is the same value on both sides#

A DateTimeOffset behaves identically in a server function and in a browser action — same instant, same offset, same comparison rule. On the wire it travels as ISO-8601 text with its offset (2026-09-14T14:30:00+05:30), which is the only textual form that keeps what the type is for.

Examples#

// A reading whose local time matters — the same instant, recorded in three places.
entity Reading {
  [Required] string Sensor;
  DateTimeOffset At;
}

string WhenLocally(Reading r) {
  return Convert.ToString(r.At.Hour) + ":" + Convert.ToString(r.At.Minute);
}

A DateTime and a DateTimeOffset in one expression — subtracted, moved and compared, in memory and in a query:

entity Delivery {
  [Required] string Parcel;
  DateTime Promised;          // a UTC instant
  DateTimeOffset Handed;      // when, and at what offset, it was handed over
}

TimeSpan Lateness(Delivery d) {
  return d.Handed - d.Promised;                  // DateTimeOffset − DateTime → TimeSpan
}

DateTimeOffset FollowUp(Delivery d) {
  return d.Handed + TimeSpan.FromDays(2);        // keeps the offset it was handed over at
}

List<Delivery> LateOnes() {
  return Delivery.Where(d => d.Handed > d.Promised).ToList();   // compared in the database, by the instant
}

DateTimeOffset Settled(Delivery d, bool handed) {
  return handed ? d.Handed : d.Promised;         // one of each → a DateTimeOffset; Promised lands at +00:00
}

See also#

Related

DateTime

A date and time. It is a wall-clock value, not an instant on a timeline, so it is never shifted by anybody's timezone…

DateOnly and TimeOnly

`DateOnly` is a calendar date (no time); `TimeOnly` is a time of day (no date) — the C# types. Build them with `new…

TimeSpan (durations)

`TimeSpan` is the duration type — a length of time, as in C#. Build one with the `TimeSpan.FromX` factories or `new…