Skip to content

[API Proposal]: TimeSpan validation methods #111847

Open
@aetos382

Description

@aetos382

Background and motivation

In many use cases, negative TimeSpan values are invalid, so you will want to make them an error during parameter validation.
However, Timeout.Infinite is an exceptional case.
If you want to perform parameter validation like this, you will need to write boilerplate code like the following.

void Foo(TimeSpan value)
{
    if (value != Timeout.Infinite)
    {
        ArgumentOutOfRangeException.ThrowIfLessThan(value, TimeSpan.Zero);
    }
}

You should not write this as follows.
This is because there are negative values that are larger than Timeout.InfiniteTimeSpan.

void Foo(TimeSpan value)
{
    ArgumentOutOfRangeException.ThrowIfLessThan(value, Timeout.InfiniteTimeSpan);

    // Foo(TimeSpan.FromTicks(-1)) prints "-00:00:00.0000001: Valid".
    Console.WriteLine($"{value}: Valid");
}

Also, ArgumentOutOfRangeException.ThrowIfNegative cannot be used when comparing with Timeout.Zero because TimeSpan does not implement INumberBase<T>.
But it is not a good idea to add a method specific to TimeSpan validation to the ArgumentOutOfRangeException class.

And, .NET timers support timeout values in milliseconds, and the valid range is from 1 to UInt32.MaxValue - 1 (0xfffffffe). 0xffffffff is not valid because it is Timeout.Infinite.
It may be useful to have a method that checks whether or not the specified value is within this valid range.
For example, it can be used by someone implementing a class that uses timers internally and wants to check the range of values in the constructor.

So, we propose adding a validation methods to the TimeSpan class.
Using these methods will make the meaning of the code clearer and also help you avoid errors in value validation.

It may also be a good idea to provide an analyzer that recommends specifications for methods like this.

API Proposal

namespace System;

public struct TimeSpan
{
    // Throws `ArgumentOutOfRangeException` if the value is negative (even if it is `Timeout.InifiniteTimeSpan`).
    public static void ThrowIfNegative(TimeSpan value, [CallerArgumentExpression] string? paramName = null);

    // Throws `ArgumentOutOfRangeException` if the value is negative (except `Timeout.InifiniteTimeSpan`).
    public static void ThrowIfNegativeExceptInfinite(TimeSpan value, [CallerArgumentExpression] string? paramName = null);

    // Throws `ArgumentOutOfRangeException` if the value is negative or `TimeSpan.Zero` (even if it is `Timeout.InifiniteTimeSpan`).
    public static void ThrowIfNegativeOrZero(TimeSpan value, [CallerArgumentExpression] string? paramName = null);

    // Throws `ArgumentOutOfRangeException` if the value is negative or `TimeSpan.Zero` (except `Timeout.InifiniteTimeSpan`).
    public static void ThrowIfNegativeOrZeroExceptInfinite(TimeSpan value, [CallerArgumentExpression] string? paramName = null);

    // Throws ArgumentOutOfRangeException if the value is not between 1 and 4294967294ms.
    public static void ThrowIfOutOfTimeoutRange(TimeSpan value, [CallerArgumentExpression] string? paramName = null);

    // Throws ArgumentOutOfRangeException if the value is not between 0 and 4294967294ms.
    public static void ThrowIfOutOfTimeoutRangeAllowsZero(TimeSpan value, [CallerArgumentExpression] string? paramName = null);

    // Throws ArgumentOutOfRangeException if the value is not between 1 and 4294967295ms.
    public static void ThrowIfOutOfTimeoutRangeAllowsInfinite(TimeSpan value, [CallerArgumentExpression] string? paramName = null);

    // Throws ArgumentOutOfRangeException if the value is not between 0 and 4294967295ms.
    public static void ThrowIfOutOfTimeoutRangeAllowsZeroAndInfinite(TimeSpan value, [CallerArgumentExpression] string? paramName = null);
}

API Usage

void Foo(TimeSpan value)
{
    // `Foo(TimeSpan.FromTicks(-1))` will result in an error.
    TimeSpan.ThrowIfNegativeExceptInfinite(value);
}

Alternative Designs

The timer itself can also have a method to check whether the value is within the valid range of the timer.
Since some of the timer implementations in .NET eventually end up with the internal TimerQueueTimer class that implements ITimer interface, this method could also be a static method of ITimer.
The upper limit supported by the timer-related methods we have proposed is currently defined in the MaxSupportedTimeout field of the System.Threading.Timer class. The ITimer interface was introduced later, along with the TimerProvider.

public interface ITimer
{
    // Throws ArgumentOutOfRangeException if the value is not between 1 and 4294967294ms.
    public static void ThrowIfOutOfTimeoutRange(TimeSpan value, [CallerArgumentExpression] string? paramName = null);

    // Throws ArgumentOutOfRangeException if the value is not between 0 and 4294967294ms.
    public static void ThrowIfOutOfTimeoutRangeAllowsZero(TimeSpan value, [CallerArgumentExpression] string? paramName = null);

    // Throws ArgumentOutOfRangeException if the value is not between 1 and 4294967295ms.
    public static void ThrowIfOutOfTimeoutRangeAllowsInfinite(TimeSpan value, [CallerArgumentExpression] string? paramName = null);

    // Throws ArgumentOutOfRangeException if the value is not between 0 and 4294967295ms.
    public static void ThrowIfOutOfTimeoutRangeAllowsZeroAndInfinite(TimeSpan value, [CallerArgumentExpression] string? paramName = null);
}

System.Windows.Forms.Timer and System.Windows.Threading.DispatcherTimer are implemented using the Win32 API SetTimer rather than TimerQueueTimer, and in this case the upper limit is different.

Risks

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions