Table of Contents

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:

  1. Time-of-day timers — register a callback for a future local LocalTime.
  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 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.LocalNowTime returns a NodaTime LocalTime; adding a Period moves the target into the future.
  • RegisterTimeOfDay(LocalTime, …) is a NodaTime-typed overload supplied by the superset; UtcTimeOfDay equivalents 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.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 BCL view of the zone used for local time-of-day registrations.
  • For deterministic NodaTime-zone tests, see the testing variant of this page — PrimeTestClock accepts a NodaTime DateTimeZone directly.