Summary#
DateTime is a date and a time of day — 2024-03-15T13:45:30 — held exactly, to 100 nanoseconds. You read its parts
(d.Year, d.DayOfWeek), do calendar arithmetic on it (d.AddMonths(1)), and construct or parse one
(new DateTime(2024, 3, 15), DateTime.Parse(s)).
Signature#
new DateTime(<int> year, <int> month, <int> day)
new DateTime(<int> year, <int> month, <int> day, <int> hour, <int> minute, <int> second)
DateTime.Parse(<string> s) -> DateTime
DateTime.MinValue -> DateTime // the earliest representable value
DateTime.MaxValue -> DateTime // the latest representable value
d.Year · d.Month · d.Day · d.Hour · d.Minute · d.Second · d.DayOfWeek · d.Date
d.AddDays(n) · d.AddMonths(n) · d.AddYears(n) · d.AddHours(n) · d.AddMinutes(n) · d.AddSeconds(n) · d.AddMilliseconds(n)
d.Millisecond · d.Microsecond · d.Nanosecond · d.TimeOfDay -> TimeSpan · d.AddMicroseconds(n)
DateTime.Compare(<DateTime> a, <DateTime> b) -> int // also d.CompareTo(other)
DateTime.DaysInMonth(<int> year, <int> month) -> int
DateTime.IsLeapYear(<int> year) -> bool
DateTime.FromOADate(<double> oa) -> DateTime // d.ToOADate() -> double
DateTime.FromFileTimeUtc(<long> fileTime) -> DateTime // d.ToFileTimeUtc() -> long
DateTime.FromBinary(<long> data) -> DateTime
d.GetDateTimeFormats() · d.GetDateTimeFormats(<char> format) -> List<string>
d.GetHashCode() -> int · d.GetTypeCode() -> TypeCodeDescription#
What are DateTime.MinValue and MaxValue?#
DateTime.MinValue and DateTime.MaxValue are the earliest and latest values a DateTime can hold. They are
ordinary values, so they compare and sort like any other date — which is what makes them useful as a starting point
for a running comparison (var earliest = DateTime.MaxValue; then keep the smaller of each candidate).
⚠ They are not a "no date" marker. A DateTime? says "no date" precisely and reads as null; a sentinel says
it by convention and every reader has to know the convention. Prefer the nullable type — a MinValue that leaks
into a UI renders as a real date in the year 1, and a MinValue that reaches a comparison silently sorts first.
It is a wall-clock value, not an instant#
2024-03-15T13:45:30 means that reading on a clock face. It is not "a moment in time as seen from a timezone", and
nothing in the platform will shift it by one. A date you store is the date you get back — the same one, in the same
digits, whether it is read on a server in Frankfurt or in a browser in São Paulo.
That is worth stating plainly because most date libraries do the opposite, and quietly.
Calendar arithmetic clamps the day#
AddMonths and AddYears move along the calendar, and clamp the day to the target month rather than overflowing
it:
new DateTime(2024, 1, 31).AddMonths(1) // 2024-02-29 — a leap year
new DateTime(2025, 1, 31).AddMonths(1) // 2025-02-28
new DateTime(2024, 2, 29).AddYears(1) // 2025-02-28None of those becomes March 2nd. If you want exactly thirty days later, say AddDays(30).
AddDays, AddHours, AddMinutes, AddSeconds and AddMilliseconds take a fractional amount and truncate it to
whole ticks (100 nanoseconds) — never rounding it to a millisecond — so
AddDays(0.5) is exactly twelve hours.
How do I read below a second, or ask about the calendar?#
d.Millisecond, d.Microsecond and d.Nanosecond are the parts below a second, as in C#: the millisecond
(0–999), the microsecond within that millisecond (0–999), and the nanosecond within that microsecond — always a
multiple of 100, because a tick is 100 nanoseconds. d.AddMicroseconds(n) moves by microseconds. d.TimeOfDay is
the time since midnight as a TimeSpan.
DateTime.Compare(a, b) and a.CompareTo(b) answer -1, 0 or 1, the way a sort comparer wants; for a plain
question, a < b reads better. DateTime.DaysInMonth(year, month) and DateTime.IsLeapYear(year) ask the
Gregorian calendar.
Can I read a date another system wrote?#
Three encodings other software hands you, each with its pair:
| encoding | read it | write it |
|---|---|---|
OLE Automation date — what Excel and older COM software store: days since 1899-12-30 as a double | DateTime.FromOADate(oa) | d.ToOADate() |
Windows FILETIME — 100-nanosecond ticks since 1601-01-01 UTC, as a long | DateTime.FromFileTimeUtc(ft) | d.ToFileTimeUtc() |
.NET's DateTime.ToBinary() serialisation, as a long | DateTime.FromBinary(data) | — |
ToBinary, ToFileTime and ToLocalTime are refused by name: they read the host's timezone or a DateTimeKind,
and a DateTime here has neither — it is a wall-clock value (above). The UTC forms are the honest ones.
d.GetDateTimeFormats() lists the date rendered in every standard format, and d.GetDateTimeFormats('d') in one —
under the invariant culture, so 'd' gives "03/15/2024". d.GetHashCode() is equal for equal dates and
d.GetTypeCode() is TypeCode.DateTime, as in C#.
DayOfWeek is the DayOfWeek enum#
d.DayOfWeek is a DayOfWeek, as in C# — DayOfWeek.Sunday through DayOfWeek.Saturday, printing as its name.
(int)d.DayOfWeek is 0 for Sunday through 6 for Saturday.
Reading the current time#
DurableClock.Now, DurableClock.UtcNow and DurableClock.Today give the current instant, and they run on the client — reading
"now" costs no round trip. That is safe here for a reason worth knowing: a DateTime is a UTC instant, not a
wall-clock reading, so the browser and the server name the same value (9am in Frankfurt is 5pm in Tokyo). Test
pinning still applies, and a durable flow that pauses and resumes still sees the instant it saw before, because the
engine resumes from a saved point rather than re-running the body from the top.
⚠ DateTime.Now is local time, and local is the APP's zone — app.DefaultTimeZone, UTC when none is declared,
never the host machine's. So in an app that declares no zone, Now and UtcNow are the same instant; in one that
declares Stockholm, Now is Stockholm's wall clock (Current time (DateTime.UtcNow, DurableClock.Now)). Anybody else's wall clock is
TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, zone) (Time zones — TimeZoneInfo). DateTime.Parse of text with an offset
("…+01:00", "…Z") answers on the same local clock, as C#'s does — the instant in the app's zone, the same instant in
UTC when none is declared — and the machine's own zone never enters it.
Everything else about a date — its parts, its arithmetic, parsing and formatting it — runs wherever you are, with no round trip.
Examples#
bool IsOverdue(DateTime due) {
return due < DurableClock.UtcNow;
}
DateTime NextBillingDate(DateTime start) {
return start.AddMonths(1);
}
// NextBillingDate(new DateTime(2024, 1, 31)) -> 2024-02-29[Test]
void Date_members_answer_what_csharp_answers() {
var d = new DateTime(2024, 3, 15, 13, 45, 30);
var fine = d.AddMicroseconds(1500);
Assert.Equal(1, fine.Millisecond);
Assert.Equal(500, fine.Microsecond);
Assert.Equal(0, fine.Nanosecond);
Assert.Equal(new TimeSpan(13, 45, 30), d.TimeOfDay);
Assert.Equal(-1, DateTime.Compare(d, d.AddDays(1)));
Assert.Equal(1, d.CompareTo(d.AddDays(-1)));
Assert.Equal(29, DateTime.DaysInMonth(2024, 2));
Assert.False(DateTime.IsLeapYear(2023));
Assert.Equal(45366.57326388889, d.ToOADate());
Assert.Equal(new DateTime(2024, 3, 15, 12, 0, 0), DateTime.FromOADate(45366.5));
Assert.Equal(133549839300000000, d.ToFileTimeUtc());
Assert.Equal(d, DateTime.FromFileTimeUtc(d.ToFileTimeUtc()));
Assert.Equal(d, DateTime.FromBinary(d.Ticks));
Assert.Equal("03/15/2024", d.GetDateTimeFormats('d')[0]);
Assert.Equal(d.GetHashCode(), new DateTime(2024, 3, 15, 13, 45, 30).GetHashCode());
Assert.Equal(TypeCode.DateTime, d.GetTypeCode());
}See also#
- decimal — the other exact value type, and the same reasoning behind it
- format specifiers — rendering a date or a number to a string
- Crossing the network — why the clock is a server operation and the arithmetic is not
- DateTimeOffset — the same instant plus the offset it was recorded at, when the local time is part of the fact