Timers, daylight saving, and testing
This article applies to both KZDev.SystemClock.PrimeTime and KZDev.PrimeTime. API shapes differ (BCL vs NodaTime types), but the ideas are shared.
Core types
IPrimeClock— Application-facing clock: current time projections, interval timers, and (on modern .NET targets) local time-of-day registrations.IPrimeTime— Narrower “time” surface; default DI registers it to the same instance asIPrimeClock.PrimeClock— Production implementation wired byAddPrimeClockunless you replace registrations.IPrimeTestClock/PrimeTestClock— Virtual time for deterministic unit tests (Advance,RunFor,Start/Stop, setters, and related members). See the API reference for constructors (they differ slightly between packages) and Test-clock control APIs for control-verb details.
Interval timers
Interval timers fire after an initial delay and optionally repeat. You register them on IPrimeClock (for example RegisterTimer / RegisterAsyncTimer). The superset adds NodaTime.Duration overloads; the SystemClock package uses TimeSpan (and matches those semantics on the shared contract).
Use IntervalTimerOptions where you need to tune behavior described in the API documentation.
Runnable examples:
- System Clock track: Interval timer (production) · Interval timer (testing)
- PrimeTime (NodaTime) track: Interval timer (production) · Interval timer (testing)
Time-of-day timers and daylight saving
On .NET 8+ (SDK NET), IPrimeClock includes local and UTC time-of-day registration APIs. Local wall-clock scheduling uses the clock’s view of the local zone and honors:
SkippedTimeBehavior— What to do when the requested local time does not exist (spring-forward gap).DuplicateTimeBehavior— What to do when the same local time occurs twice (fall-back overlap).
Configure these on DayTimeTimerOptions. UTC time-of-day registrations do not use skipped/duplicate behaviors.
Note: Some members are omitted from
netstandard2.0builds. If you target older TFMs, confirm availability in the API reference for your target.
Runnable examples:
- System Clock track: Time-of-day and DST (production)
- PrimeTime (NodaTime) track: Time-of-day and DST (production)
Each track's page covers the time-of-day timer registration, environment-aware skipped/ambiguous local-time probes, and the clock's local schedule zone.
Bridging to TimeProvider
Both stacks can expose a TimeProvider that delegates to an IPrimeClock via PrimeClockTimeProviderExtensions.ToTimeProvider. That helps integrate with APIs that expect TimeProvider while keeping PrimeTime as the source of truth (especially under a PrimeTestClock).
Test clock runner and marching
PrimeTestClock can advance time in three ways:
- Explicit control —
Advanceand forwardSetTime/SetInstant/SetLocalTimesynchronously march virtual time to each earliest due instant, dispatch delays, time expiries, and timers at that instant, and raiseClockEventsonce per distinct virtual instant visited. Timer callbacks seeUtcNow/NowInstantat the firing instant, not only the final horizon. - Automatic runner —
Start(perSecondRate)runs an unbounded background loop that waits for the next virtual deadline (including a virtual one-minuteClockEventsheartbeat when idle), scales wait duration byperSecondRate, and catches up using measured real elapsed time after each wait. Run rate must be between 100 ms and 1 hour of virtual time per real second (inclusive). - Bounded automatic runner —
RunFor(duration)andRunFor(duration, perSecondRate)start automatic progression immediately and return without blocking. The run stops when virtual time reaches the stop horizon and raisesClockStopped.
While the runner is active:
- "Now" projection —
UtcNowDateTimeOffset,LocalNow*, and NodaNowInstantlinearly project from a committed virtual instant and a sharedStopwatchanchor at the run rate. - Persist-on-read — Reading "now" marches through due work up to the projected instant, commits storage, and restarts the anchor (frequent reads can be costly).
- 15 ms rule — Intended real sleeps shorter than 15 ms are handled by bursting virtual steps without sleeping.
- Wake on new work — New delays, timers, or time expiries, cancellations,
Stop, or persist-on-read can wake the runner so sooner deadlines are not missed.
Backward moves: forbidden while running; while stopped, forbidden if any active interval timer exists; otherwise an instant jump with one ClockEvents and day-time NextDueUtc recomputation (at most one callback per discrete time-of-day occurrence).
Full control-verb reference: Test-clock control APIs. API details: IPrimeTestClock (System Clock) · IPrimeTestClock (Noda).
Testing tips
- Replace
IPrimeClock(orPrimeClock) withPrimeTestClockin tests. - Drive time with
Advance/RunForwhen you need synchronous, deterministic steps; useStartwhen you want real-time-paced automatic advancement (andStopto return to manual control). - For daylight saving edge cases around local wall times, the NodaTime
PrimeTestClockconstructor that accepts aDateTimeZoneis the most direct way to model zone rules in tests. The SystemClock package exposesLocalScheduleTimeZoneonIPrimeClockfor BCL-oriented local scheduling; align that with your test scenario.
Testing examples:
- System Clock track: Time-of-day and DST (testing)
- PrimeTime (NodaTime) track: Time-of-day and DST (testing)
- Cross-track: Test-clock control APIs · DI replacement with
AddPrimeTestClock