Time-of-day and DST (production) — System Clock stack
This page combines three runnable demos that all touch local wall-clock time on the System Clock stack:
- Time-of-day timers — register a callback for a future local
TimeOnly. - 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 time-of-day. 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>();
TimeOnly now = primeClock.LocalNowTimeOnly;
TimeOnly target = now.Add(_runMode == DemoRunMode.Long
? TimeSpan.FromSeconds(8)
: TimeSpan.FromSeconds(4));
LocalTimeOfDay timeOfDay = new(target);
TaskCompletionSource syncCompleted = new(TaskCreationOptions.RunContinuationsAsynchronously);
TaskCompletionSource asyncCompleted = new(TaskCreationOptions.RunContinuationsAsynchronously);
ScenarioConsole.WriteLine($"Starting sync time-of-day timer for local time {timeOfDay.ToLongTimeString()}.");
IClockDayTimeTimer syncTimer = primeClock.RegisterTimeOfDay(timeOfDay,
(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 {timeOfDay.ToLongTimeString()}.");
IClockDayTimeTimer asyncTimer = primeClock.RegisterAsyncTimeOfDay(timeOfDay,
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:
LocalTimeOfDaywraps aTimeOnlytarget. UseUtcTimeOfDayif you want UTC-anchored scheduling instead.RegisterTimeOfDay/RegisterAsyncTimeOfDayreturn anIClockDayTimeTimer; dispose it to stop pending callbacks.- 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.
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 zone used to interpret local time-of-day registrations.- This member is specific to the System Clock stack. The PrimeTime / NodaTime stack provides a NodaTime-based equivalent via the
PrimeTestClockzone constructor andDayTimeTimerOptions.
Related
- Time-of-day and DST (testing) — DST behavior under a virtual clock.
- Concepts: Timers, daylight saving, and testing
- API:
IPrimeClock·DayTimeTimerOptions·IClockDayTimeTimer