Table of Contents

Interval timer (production) — PrimeTime (NodaTime) stack

This example demonstrates registering both synchronous and asynchronous interval timer callbacks on IPrimeClock using the PrimeTime (NodaTime) package (KZDev.PrimeTime). The timer signatures use NodaTime Duration for due time and repeat interval — the superset adds these overloads on top of the shared TimeSpan API.

The snippet below is the body of the demo RunAsync method. It builds a minimal ServiceCollection, resolves an IPrimeClock, registers two timers (one synchronous, one asynchronous), waits for both to fire twice, and disposes both registrations.

ServiceCollection services = [];
services.AddPrimeClock();

await using ServiceProvider serviceProvider = services.BuildServiceProvider();
IPrimeClock primeClock = serviceProvider.GetRequiredService<IPrimeClock>();

Duration dueTime = _runMode == DemoRunMode.Long
    ? Duration.FromSeconds(1)
    : Duration.FromMilliseconds(300);

Duration repeat = _runMode == DemoRunMode.Long
    ? Duration.FromSeconds(2)
    : Duration.FromMilliseconds(700);

TaskCompletionSource syncCompleted = new(TaskCreationOptions.RunContinuationsAsynchronously);
TaskCompletionSource asyncCompleted = new(TaskCreationOptions.RunContinuationsAsynchronously);

int syncCount = 0;
int asyncCount = 0;

ScenarioConsole.WriteLine($"Starting sync interval timer (due: {dueTime}, repeat: {repeat}).");
IClockIntervalTimer syncTimer = primeClock.RegisterTimer(dueTime, repeat,
    (context, callbackCancellationToken) =>
    {
        callbackCancellationToken.ThrowIfCancellationRequested();
        int currentCount = Interlocked.Increment(ref syncCount);
        ScenarioConsole.WriteLine($"sync interval callback #{currentCount} on registration {context.Registration.Id}");
        if (currentCount >= 2)
        {
            syncCompleted.TrySetResult();
        }
    },
    cancellationToken, state: "sync timer", timerOptions: null);

ScenarioConsole.WriteLine($"Starting async interval timer (due: {dueTime}, repeat: {repeat}).");
IClockIntervalTimer asyncTimer = primeClock.RegisterAsyncTimer(dueTime, repeat,
    async (context, callbackCancellationToken) =>
    {
        callbackCancellationToken.ThrowIfCancellationRequested();
        int currentCount = Interlocked.Increment(ref asyncCount);
        ScenarioConsole.WriteLine($"async interval callback #{currentCount} on registration {context.Registration.Id}");
        if (currentCount >= 2)
        {
            asyncCompleted.TrySetResult();
        }

        await ValueTask.CompletedTask;
    },
    cancellationToken, state: "async timer", timerOptions: null);

ScenarioConsole.WriteLine("Waiting for both interval timers to fire twice...");
await Task.WhenAll(syncCompleted.Task, asyncCompleted.Task);
ScenarioConsole.WriteLine("Both interval timers completed required callback count.");

syncTimer.Dispose();
asyncTimer.Dispose();

What to notice

  • AddPrimeClock registers NodaTime.IClock (defaulting to SystemClock.Instance), IPrimeClock (singleton PrimeClock), and IPrimeTime (same instance as IPrimeClock).
  • Both RegisterTimer (sync) and RegisterAsyncTimer (async) accept the same (dueTime, repeat, callback, cancellationToken, state, timerOptions) shape with Duration arguments. The shared TimeSpan overloads remain available for cross-stack code.
  • Each timer registration returns an IClockIntervalTimer that you must dispose to stop the timer and release resources.
  • The callback receives an IClockIntervalTimerCallbackContext (used here for Registration.Id in logging) and a per-callback CancellationToken signalled when the timer is being disposed or the registration cancellation token fires.