dotnet/reactive · error · ArgumentNullException

durationSelector

Error message

durationSelector

What it means

This ArgumentNullException is thrown synchronously when the durationSelector argument is null. durationSelector decides how long each group lives (it returns the observable whose completion closes the group), so the operator cannot function without it and rejects null immediately at call time.

Solutions

  1. Provide a duration selector, e.g. g => g.TakeUntil(timerObservable).
  2. If group lifetime should be unbounded, use GroupBy instead of passing a null duration selector.
  3. Null-check any dynamically built selector before invoking GroupByUntil.

Example fix

// before
var grouped = source.GroupByUntil(x => ValueTask.FromResult(x.Id), (Func<IGroupedAsyncObservable<int, T>, IAsyncObservable<Unit>>)null);
// after
var grouped = source.GroupByUntil(
    x => ValueTask.FromResult(x.Id),
    g => AsyncObservable.Timer(TimeSpan.FromMinutes(5)).Select(_ => Unit.Default));
Defensive patterns

Strategy: validation

Validate before calling

if (durationSelector is null)
{
    durationSelector = g => AsyncObservable.Timer(TimeSpan.FromMinutes(5)).Select(_ => Unit.Default);
}

Type guard

static bool IsValidSelector<TG, TD>(Func<TG, IAsyncObservable<TD>>? f) => f is not null;

Try / catch

try
{
    var grouped = source.GroupByUntil(keySelector, durationSelector);
}
catch (ArgumentNullException ex) when (ex.ParamName == "durationSelector")
{
    // build a default duration selector or report misconfiguration
}

Prevention

When it happens

Trigger: Calling GroupByUntil with a valid keySelector but null durationSelector, e.g. source.GroupByUntil(keySelector, null) or source.GroupByUntil(keySelector, null, capacity).

Common situations: Overload resolution confusion when adding a capacity/comparer argument shifts positional parameters and the duration lambda slot ends up null; a computed selector variable left uninitialized.

Related errors


AI-assisted analysis of dotnet/reactive@94b5d5ab91 (2026-09-15). Data as JSON: /api/errors/b12b8ac04501d1b5. Report an issue: GitHub.

Appendix: source

Thrown at AsyncRx.NET/System.Reactive.Async/Linq/Operators/GroupByUntil.cs:199

            if (durationSelector == null)
                throw new ArgumentNullException(nameof(durationSelector));
            if (comparer == null)
                throw new ArgumentNullException(nameof(comparer));

            return CreateAsyncObservable<IGroupedAsyncObservable<TKey, TSource>>.From(
                source,
                (keySelector, durationSelector, comparer),
                static (source, state, observer) => GroupByUntilCore<TSource, TKey, TSource, TDuration>(source, observer, (o, d) => AsyncObserver.GroupByUntil(o, d, state.keySelector, state.durationSelector, state.comparer)));
        }

        public static IAsyncObservable<IGroupedAsyncObservable<TKey, TSource>> GroupByUntil<TSource, TKey, TDuration>(this IAsyncObservable<TSource> source, Func<TSource, ValueTask<TKey>> keySelector, Func<IGroupedAsyncObservable<TKey, TSource>, IAsyncObservable<TDuration>> durationSelector, int capacity)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            if (keySelector == null)
                throw new ArgumentNullException(nameof(keySelector));
            if (durationSelector == null)
                throw new ArgumentNullException(nameof(durationSelector));
            if (capacity < 0)
                throw new ArgumentOutOfRangeException(nameof(capacity));

            return CreateAsyncObservable<IGroupedAsyncObservable<TKey, TSource>>.From(
                source,
                (keySelector, durationSelector, capacity),
                static (source, state, observer) => GroupByUntilCore<TSource, TKey, TSource, TDuration>(source, observer, (o, d) => AsyncObserver.GroupByUntil(o, d, state.keySelector, state.durationSelector, state.capacity)));
        }

        public static IAsyncObservable<IGroupedAsyncObservable<TKey, TSource>> GroupByUntil<TSource, TKey, TDuration>(this IAsyncObservable<TSource> source, Func<TSource, ValueTask<TKey>> keySelector, Func<IGroupedAsyncObservable<TKey, TSource>, IAsyncObservable<TDuration>> durationSelector, int capacity, IEqualityComparer<TKey> comparer)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            if (keySelector == null)
                throw new ArgumentNullException(nameof(keySelector));
            if (durationSelector == null)
                throw new ArgumentNullException(nameof(durationSelector));
            if (capacity < 0)

View on GitHub (pinned to 94b5d5ab91)