How it works
Not every date-like value is an instant. A birthday, invoice date, or service day can be a calendar date without a time or zone. DateOnly represents that shape directly. TimeOnly represents a time of day without attaching a date. Choosing a type that matches the domain prevents an arbitrary midnight timestamp from acquiring accidental time-zone semantics later.
These types answer different questions from DateTimeOffset. A date can be compared to another date, and a time can be compared within a daily schedule, but neither alone identifies a universal instant. When an application eventually schedules an event, it must combine the required date, time, and zone policy intentionally. Avoid adding missing information through a device default merely because a timestamp API requires it.
Walk through the example. The example begins on the final day of a month and adds one day. DateOnly handles the calendar transition to the first day of the next month. A TimeOnly value then advances by ninety minutes across midnight, resulting in zero thirty. Formatting is explicit and invariant so the example prints the same text regardless of the user's preferred date and time style.
Watch the boundary. A time-of-day calculation can wrap into another day. If that day change matters, do not retain only the wrapped TimeOnly value and silently discard the overflow. Similarly, parsing a date through a culture-dependent short format can swap day and month meanings. Prefer a documented format and ParseExact or TryParseExact at a machine-format boundary.
Practice independently. Build a due-date calculator that adds a fixed number of calendar days to a DateOnly value. Test a month boundary and a leap-year February boundary. Separately model an opening and closing time, and define whether an overnight interval is permitted. Explain why calculating elapsed hours across a real daylight-saving transition would need more information than DateOnly and TimeOnly provide by themselves.
A working example
using System;
using System.Collections.Generic;
using System.Linq;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
var date = new DateOnly(2026, 1, 31);
Console.WriteLine(date.AddDays(1).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
var time = new TimeOnly(23, 0);
Console.WriteLine(time.AddMinutes(90).ToString("HH:mm", CultureInfo.InvariantCulture));
Console.WriteLine(new DateOnly(2024, 2, 28).AddDays(1).ToString("yyyy-MM-dd", CultureInfo.InvariantCulture));
Expected output
2026-02-01 00:30 2024-02-29
Choose calendar and clock types deliberately, and preserve day overflow when the domain needs it.