{"record":{"id":"6ba21f24b9d9c4e3","repo":"dotnet/reactive","slug":"argumentnullexception-durationselector","errorCode":null,"errorMessage":"ArgumentNullException: durationSelector","messagePattern":"ArgumentNullException: durationSelector","errorType":"validation","errorClass":"ArgumentNullException","httpStatus":null,"severity":"error","filePath":"AsyncRx.NET/System.Reactive.Async/Linq/Operators/GroupByUntil.cs","lineNumber":358,"sourceCode":"                throw new ArgumentNullException(nameof(keySelector));\n            if (durationSelector == null)\n                throw new ArgumentNullException(nameof(durationSelector));\n            if (comparer == null)\n                throw new ArgumentNullException(nameof(comparer));\n\n            return GroupByUntil(observer, subscription, keySelector, durationSelector, int.MaxValue, comparer);\n        }\n\n        public static ValueTask<(IAsyncObserver<TSource>, IAsyncDisposable)> GroupByUntil<TSource, TKey, TDuration>(IAsyncObserver<IGroupedAsyncObservable<TKey, TSource>> observer, IAsyncDisposable subscription, Func<TSource, TKey> keySelector, Func<IGroupedAsyncObservable<TKey, TSource>, IAsyncObservable<TDuration>> durationSelector, int capacity)\n        {\n            if (observer == null)\n                throw new ArgumentNullException(nameof(observer));\n            if (subscription == null)\n                throw new ArgumentNullException(nameof(subscription));\n            if (keySelector == null)\n                throw new ArgumentNullException(nameof(keySelector));\n            if (durationSelector == null)\n                throw new ArgumentNullException(nameof(durationSelector));\n            if (capacity < 0)\n                throw new ArgumentOutOfRangeException(nameof(capacity));\n\n            return GroupByUntil(observer, subscription, keySelector, durationSelector, capacity, EqualityComparer<TKey>.Default);\n        }\n\n        public static ValueTask<(IAsyncObserver<TSource>, IAsyncDisposable)> GroupByUntil<TSource, TKey, TDuration>(IAsyncObserver<IGroupedAsyncObservable<TKey, TSource>> observer, IAsyncDisposable subscription, Func<TSource, TKey> keySelector, Func<IGroupedAsyncObservable<TKey, TSource>, IAsyncObservable<TDuration>> durationSelector, int capacity, IEqualityComparer<TKey> comparer)\n        {\n            if (observer == null)\n                throw new ArgumentNullException(nameof(observer));\n            if (subscription == null)\n                throw new ArgumentNullException(nameof(subscription));\n            if (keySelector == null)\n                throw new ArgumentNullException(nameof(keySelector));\n            if (durationSelector == null)\n                throw new ArgumentNullException(nameof(durationSelector));\n            if (capacity < 0)\n                throw new ArgumentOutOfRangeException(nameof(capacity));","sourceCodeStart":340,"sourceCodeEnd":376,"githubUrl":"https://github.com/dotnet/reactive/blob/94b5d5ab912789f5abe9a72138a25bbd716fe59c/AsyncRx.NET/System.Reactive.Async/Linq/Operators/GroupByUntil.cs#L340-L376","documentation":"System.Reactive.Async's GroupByUntil overload with a capacity parameter validates all delegate arguments before doing any work. The durationSelector delegate, which maps each grouped observable to a duration observable that controls when the group completes, was passed as null. The library throws ArgumentNullException eagerly so the failure surfaces at call time rather than silently breaking group lifetime logic later.","triggerScenarios":"Calling the GroupByUntil(observer, subscription, keySelector, durationSelector, capacity) overload (GroupByUntil.cs:349) with a null durationSelector argument, typically because the duration lambda was left out or a nullable variable holding the delegate was never assigned.","commonSituations":"Building a query conditionally where the duration selector is only sometimes assigned; refactoring that removed the duration lambda; passing a method group that resolves to null via a factory; calling GroupByUntil dynamically through reflection or DI where arguments are constructed at runtime.","solutions":["Pass a non-null duration selector, e.g. grp => grp.Take(1), that returns the duration observable for each group","Check the variable intended as durationSelector for a null value before calling the operator","If a default group lifetime is intended, use a simpler GroupBy overload or supply grp => Observable.Never<TDuration>() equivalent so groups stay open until the source completes"],"exampleFix":"// before\nvar res = source.GroupByUntil(x => x.Key, null, 16);\n// after\nvar res = source.GroupByUntil(x => x.Key, grp => grp.TakeUntil(closeSignal), 16);","handlingStrategy":"validation","validationCode":"if (durationSelector == null) throw new ArgumentException(\"durationSelector must be provided before calling GroupByUntil\", nameof(durationSelector));","typeGuard":"bool IsValidDurationSelector<TSource,TKey,TDuration>(Func<IGroupedAsyncObservable<TKey,TSource>, IAsyncObservable<TDuration>>? f) => f is not null;","tryCatchPattern":"try { var res = source.GroupByUntil(keySelector, durationSelector, capacity); }\ncatch (ArgumentNullException ex) when (ex.ParamName == \"durationSelector\") { /* supply a default duration selector and retry */ }","preventionTips":["Never pass nullable delegate fields directly into Rx operators","Prefer overload resolution sanity: list lambda arguments inline so you cannot omit the duration selector","Write a unit test that constructs each query to catch unassigned delegates"],"tags":["csharp","argument-null","async-rx","groupbyuntil"],"backgroundTag":"null-argument","analyzedSha":"94b5d5ab912789f5abe9a72138a25bbd716fe59c","analyzedAt":"2026-09-15T02:26:24.759Z","contentChangedAt":"2026-09-15T02:26:24.759Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}