dotnet/reactive · error · ArgumentNullException

ArgumentNullException(nameof(comparer))

Error message

ArgumentNullException(nameof(comparer))

What it means

The sync-keySelector DistinctUntilChanged<TSource,TKey> overload throws ArgumentNullException when the IEqualityComparer<TKey> comparer is null. The operator calls comparer.Equals on every notification, so a null comparer would crash mid-stream; it is rejected eagerly instead.

Solutions

  1. Pass EqualityComparer<TKey>.Default when you want default semantics instead of null.
  2. Pass a concrete comparer instance (e.g. StringComparer.Ordinal).
  3. If the comparer is resolved at runtime, fall back to EqualityComparer<TKey>.Default when resolution returns null.

Example fix

// before
var op = AsyncObserver.DistinctUntilChanged(observer, x => x.Name, cfg.Comparer); // null
// after
var cmp = cfg.Comparer ?? EqualityComparer<string>.Default;
var op = AsyncObserver.DistinctUntilChanged(observer, x => x.Name, cmp);
Defensive patterns

Strategy: validation

Validate before calling

var cmp2 = comparer ?? EqualityComparer<TKey>.Default;
var res = AsyncObserver.DistinctUntilChanged(observer, keySelector, cmp2);

Type guard

static IEqualityComparer<TKey> OrDefault<TKey>(IEqualityComparer<TKey>? c) => c ?? EqualityComparer<TKey>.Default;

Try / catch

try { var res = AsyncObserver.DistinctUntilChanged(observer, keySelector, comparer); }
catch (ArgumentNullException ex) when (ex.ParamName == "comparer") { comparer = EqualityComparer<TKey>.Default; }

Prevention

When it happens

Trigger: Calling DistinctUntilChanged(observer, keySelector, null) with an explicit comparer argument that is null.

Common situations: A comparer resolved from a registry/DI container that returned null; a nullable comparer field; callers intending 'default comparer' passing null instead of EqualityComparer<TKey>.Default.

Related errors


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

Appendix: source

Thrown at AsyncRx.NET/System.Reactive.Async/Linq/Operators/DistinctUntilChanged.cs:137

        public static IAsyncObserver<TSource> DistinctUntilChanged<TSource, TKey>(IAsyncObserver<TSource> observer, Func<TSource, ValueTask<TKey>> keySelector)
        {
            if (observer == null)
                throw new ArgumentNullException(nameof(observer));
            if (keySelector == null)
                throw new ArgumentNullException(nameof(keySelector));

            return DistinctUntilChanged(observer, keySelector, EqualityComparer<TKey>.Default);
        }

        public static IAsyncObserver<TSource> DistinctUntilChanged<TSource, TKey>(IAsyncObserver<TSource> observer, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
        {
            if (observer == null)
                throw new ArgumentNullException(nameof(observer));
            if (keySelector == null)
                throw new ArgumentNullException(nameof(keySelector));
            if (comparer == null)
                throw new ArgumentNullException(nameof(comparer));

            var hasCurrentKey = false;
            var currentKey = default(TKey);

            return Create<TSource>(
                async x =>
                {
                    var key = default(TKey);

                    try
                    {
                        key = keySelector(x);
                    }
                    catch (Exception ex)
                    {
                        await observer.OnErrorAsync(ex).ConfigureAwait(false);
                        return;
                    }

View on GitHub (pinned to 94b5d5ab91)