Time-of-day and DST (production) — PrimeTime (NodaTime) stack
This page combines three runnable demos that all touch local wall-clock time on the PrimeTime (NodaTime) stack:
- Time-of-day timers — register a callback for a future local
LocalTime. - Environment-aware DST — probe the local
TimeZoneInfofor spring-forward (invalid) and fall-back (ambiguous) wall-clock samples. - Local schedule zone — read the timezone the clock uses for local scheduling decisions.
Time-of-day timers
Schedule a one-shot callback for a future local LocalTime. The example registers both a synchronous and an asynchronous callback, then waits for both to fire.
ServiceCollection services = [];
services.AddPrimeClock();
await using ServiceProvider serviceProvider = services.BuildServiceProvider();
IPrimeClock primeClock = serviceProvider.GetRequiredService<IPrimeClock>();
LocalTime targetTime = primeClock.LocalNowTime.Plus(_runMode == DemoRunMode.Long
? Period.FromSeconds(8)
: Period.FromSeconds(4));
TaskCompletionSource syncCompleted = new(TaskCreationOptions.RunContinuationsAsynchronously);
TaskCompletionSource asyncCompleted = new(TaskCreationOptions.RunContinuationsAsynchronously);
ScenarioConsole.WriteLine($"Starting sync time-of-day timer for local time {targetTime}.");
IClockDayTimeTimer syncTimer = primeClock.RegisterTimeOfDay(targetTime,
(context, callbackCancellationToken) =>
{
callbackCancellationToken.ThrowIfCancellationRequested();
ScenarioConsole.WriteLine($"sync time-of-day callback fired. RegistrationId={context.Registration.Id}");
syncCompleted.TrySetResult();
},
cancellationToken, state: "sync time-of-day", timerOptions: null);
ScenarioConsole.WriteLine($"Starting async time-of-day timer for local time {targetTime}.");
IClockDayTimeTimer asyncTimer = primeClock.RegisterAsyncTimeOfDay(targetTime,
async (context, callbackCancellationToken) =>
{
callbackCancellationToken.ThrowIfCancellationRequested();
ScenarioConsole.WriteLine($"async time-of-day callback fired. RegistrationId={context.Registration.Id}");
asyncCompleted.TrySetResult();
await ValueTask.CompletedTask;
},
cancellationToken, state: "async time-of-day", timerOptions: null);
ScenarioConsole.WriteLine("Waiting for both time-of-day timers to fire...");
await Task.WhenAll(syncCompleted.Task, asyncCompleted.Task);
ScenarioConsole.WriteLine("Both time-of-day timers have fired.");
syncTimer.Dispose();
asyncTimer.Dispose();
What to notice:
primeClock.LocalNowTimereturns a NodaTimeLocalTime; adding aPeriodmoves the target into the future.RegisterTimeOfDay(LocalTime, …)is a NodaTime-typed overload supplied by the superset;UtcTimeOfDayequivalents are also available.- Skipped/duplicate-time behavior (DST gaps and overlaps) is configured on
DayTimeTimerOptions, omitted here for brevity.
Environment-aware DST
This demo probes TimeZoneInfo.Local for representative invalid (spring-forward gap) and ambiguous (fall-back overlap) calendar samples and prints what the BCL reports for each. NodaTime is also available for richer zone math, but this scenario stays on BCL types so the output mirrors the System Clock variant on the same host.
TimeZoneInfo localZone = TimeZoneInfo.Local;
if (!_context.SupportsDaylightSavingTime)
{
ScenarioConsole.WriteLine("Local timezone does not expose DST transitions on this machine.");
return Task.CompletedTask;
}
if (_context.InvalidLocalTimeExample is DateTime invalidLocalTime)
{
bool isInvalid = localZone.IsInvalidTime(invalidLocalTime);
ScenarioConsole.WriteLine($"Invalid local time sample: {invalidLocalTime:yyyy-MM-dd HH:mm:ss} (IsInvalidTime={isInvalid})");
}
else
{
ScenarioConsole.WriteLine("No invalid-time sample could be computed for the current environment.");
}
if (_context.AmbiguousLocalTimeExample is DateTime ambiguousLocalTime)
{
bool isAmbiguous = localZone.IsAmbiguousTime(ambiguousLocalTime);
ScenarioConsole.WriteLine($"Ambiguous local time sample: {ambiguousLocalTime:yyyy-MM-dd HH:mm:ss} (IsAmbiguousTime={isAmbiguous})");
if (!isAmbiguous)
{
return Task.CompletedTask;
}
TimeSpan[] offsets = localZone.GetAmbiguousTimeOffsets(ambiguousLocalTime);
ScenarioConsole.WriteLine($"Ambiguous offsets: {string.Join(", ", offsets)}");
}
else
{
ScenarioConsole.WriteLine("No ambiguous-time sample could be computed for the current environment.");
}
return Task.CompletedTask;
What to notice:
- The example only runs the deeper checks when
TimeZoneInfo.Local.SupportsDaylightSavingTimeistrue— many CI containers reportfalse. IsInvalidTimereturnstruefor clock values inside a spring-forward gap (those calendar values do not exist in the local zone).IsAmbiguousTime+GetAmbiguousTimeOffsetsdescribe fall-back overlaps where the same calendar value occurs at two distinct UTC offsets.
Local schedule zone
Confirms the timezone IPrimeClock.LocalScheduleTimeZone uses for local wall-clock scheduling. Customizing this lets you decouple DST scheduling from the host's TimeZoneInfo.Local.
ServiceCollection services = [];
services.AddPrimeClock();
using ServiceProvider serviceProvider = services.BuildServiceProvider();
IPrimeClock primeClock = serviceProvider.GetRequiredService<IPrimeClock>();
TimeZoneInfo localScheduleZone = primeClock.LocalScheduleTimeZone;
ScenarioConsole.WriteLine($"Clock LocalScheduleTimeZone: {localScheduleZone.Id}");
ScenarioConsole.WriteLine($"Clock zone supports DST: {localScheduleZone.SupportsDaylightSavingTime}");
return Task.CompletedTask;
What to notice:
LocalScheduleTimeZoneis the BCL view of the zone used for local time-of-day registrations.- For deterministic NodaTime-zone tests, see the testing variant of this page —
PrimeTestClockaccepts a NodaTimeDateTimeZonedirectly.
Related
- Time-of-day and DST (testing) — DST behavior under a virtual clock.
- Concepts: Timers, daylight saving, and testing
- API:
IPrimeClock·DayTimeTimerOptions·IClockDayTimeTimer