Table of Contents

Interval timer (production) — System Clock stack

This example demonstrates registering both synchronous and asynchronous interval timer callbacks on IPrimeClock using the System Clock package (KZDev.SystemClock.PrimeTime). The timer signatures use TimeSpan for due time and repeat interval — the BCL convention this stack standardizes on.

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>();

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

TimeSpan repeat = _runMode == DemoRunMode.Long
    ? TimeSpan.FromSeconds(2)
    : TimeSpan.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 TimeProvider.System, 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.
  • 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.