Table of Contents

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:

  1. Time-of-day timers — register a callback for a future local TimeOnly.
  2. Environment-aware DST — probe the local TimeZoneInfo for spring-forward (invalid) and fall-back (ambiguous) wall-clock samples.
  3. 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:

  • LocalTimeOfDay wraps a TimeOnly target. Use UtcTimeOfDay if you want UTC-anchored scheduling instead.
  • RegisterTimeOfDay / RegisterAsyncTimeOfDay return an IClockDayTimeTimer; 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.SupportsDaylightSavingTime is true — many CI containers report false.
  • IsInvalidTime returns true for clock values inside a spring-forward gap (those calendar values do not exist in the local zone).
  • IsAmbiguousTime + GetAmbiguousTimeOffsets describe 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:

  • LocalScheduleTimeZone is 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 PrimeTestClock zone constructor and DayTimeTimerOptions.