Table of Contents

Class ObservableTimeProvider

Namespace
TimeAssertions
Assembly
TimeAssertions.dll

A TimeProvider decorator that tracks the ITimer instances created against it so tests can assert on timer-disposal behaviour ("did this hosted service dispose every timer it started?"). Every non-timer member delegates verbatim to the wrapped inner provider, so it composes with any provider, most usefully a FakeTimeProvider: advance fake time on the inner provider and assert on the active-timer set exposed here.

public sealed class ObservableTimeProvider : TimeProvider
Inheritance
ObservableTimeProvider
Inherited Members
Extension Methods

Remarks

This fills the gap that FakeTimeProvider leaves open. The BCL fake does not surface the timers created against it (https://github.com/dotnet/extensions/issues/7515), so timer leaks in BackgroundService / IHostedService code cannot otherwise be asserted without reflection. This wrapper observes CreateTimer(TimerCallback, object?, TimeSpan, TimeSpan), callback fires (via TimerFireCount), and timer disposal directly, staying allocation-light and reflection-free. If the upstream proposal lands, the implementation can switch to the BCL API behind the same assertion surface with no consumer change.

All members are thread-safe: production code under test typically creates and disposes timers from background threads while the assertion reads the active set from the test thread.

Constructors

ObservableTimeProvider(TimeProvider)

Initializes a new ObservableTimeProvider that delegates every non-timer operation to inner and intercepts timer creation for leak tracking.

public ObservableTimeProvider(TimeProvider inner)

Parameters

inner TimeProvider

The provider to wrap. Typically a FakeTimeProvider in tests.

Exceptions

ArgumentNullException

inner is null.

Properties

ActiveTimerCount

Gets the number of timers created through this provider that have not yet been disposed.

public int ActiveTimerCount { get; }

Property Value

int

ActiveTimers

Gets a point-in-time snapshot of the still-active timers, each described by the schedule it currently carries. The returned list is a copy taken under the internal lock; later creations or disposals do not mutate it. Order is unspecified; the failure-message renderer sorts deterministically.

public IReadOnlyList<ActiveTimerInfo> ActiveTimers { get; }

Property Value

IReadOnlyList<ActiveTimerInfo>

LocalTimeZone

Gets a TimeZoneInfo object that represents the local time zone according to this TimeProvider's notion of time.

public override TimeZoneInfo LocalTimeZone { get; }

Property Value

TimeZoneInfo

NextTimerDueTime

Gets the due time of the next pending timer: the smallest DueTime among the still-active timers that are currently enabled, or null when no enabled timer is pending. A timer whose due time is InfiniteTimeSpan is disabled (its callback will not fire until a Change(TimeSpan, TimeSpan) re-arms it) and is excluded from the calculation.

public TimeSpan? NextTimerDueTime { get; }

Property Value

TimeSpan?

Remarks

This reads the schedule the timer carries without advancing the clock, so a test can assert which delay a loop just scheduled (for example the next step of an exponential backoff) rather than advancing fake time and inferring the delay from when the callback fires. The due time is the value supplied at CreateTimer(TimerCallback, object, TimeSpan, TimeSpan), the most recent Change(TimeSpan, TimeSpan), or the timer's period once it has fired (a periodic timer re-arms at its period; a one-shot becomes disabled); it is relative to the moment that schedule was set, not a remaining countdown.

The value is computed under the internal lock, so it is a consistent snapshot even while background code creates, changes, or disposes timers concurrently.

TimerFireCount

Gets the cumulative number of times a timer callback has fired across every timer created through this provider, counted from construction. The count is never decremented, so it survives a timer's disposal: a heartbeat loop that fired three times and then disposed its timer still reports 3. With a FakeTimeProvider as the inner provider a fire happens only when test code advances fake time past a timer's due (or period) boundary, so this is a deterministic count of the callbacks the advances triggered, not a wall-clock race.

public long TimerFireCount { get; }

Property Value

long

TimestampFrequency

Gets the frequency of GetTimestamp() of high-frequency value per second.

public override long TimestampFrequency { get; }

Property Value

long

Methods

CreateTimer(TimerCallback, object?, TimeSpan, TimeSpan)

Creates a new ITimer instance, using TimeSpan values to measure time intervals.

public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period)

Parameters

callback TimerCallback

A delegate representing a method to be executed when the timer fires. The method specified for callback should be reentrant, as it may be invoked simultaneously on two threads if the timer fires again before or while a previous callback is still being handled.

state object

An object to be passed to the callback. This may be null.

dueTime TimeSpan

The amount of time to delay before callback is invoked. Specify InfiniteTimeSpan to prevent the timer from starting. Specify Zero to start the timer immediately.

period TimeSpan

The time interval between invocations of callback. Specify InfiniteTimeSpan to disable periodic signaling.

Returns

ITimer

The newly created ITimer instance.

Exceptions

ArgumentNullException

callback is null.

ArgumentOutOfRangeException

The number of milliseconds in the value of dueTime or period is negative and not equal to Infinite, or is greater than MaxValue.

GetTimestamp()

Gets the current high-frequency value designed to measure small time intervals with high accuracy in the timer mechanism.

public override long GetTimestamp()

Returns

long

A long integer representing the high-frequency counter value of the underlying timer mechanism.

GetUtcNow()

Gets a DateTimeOffset value whose date and time are set to the current Coordinated Universal Time (UTC) date and time and whose offset is Zero, all according to this TimeProvider's notion of time.

public override DateTimeOffset GetUtcNow()

Returns

DateTimeOffset