From 577990432768fcea0d2d8c7f0ded6bd36a720670 Mon Sep 17 00:00:00 2001 From: "Darrin W. Cullop" Date: Sun, 20 Sep 2026 22:06:07 -0700 Subject: [PATCH 1/2] Restore named timer BatchIf overload compatibility --- ...ts.DynamicDataTests.DotNet8_0.verified.txt | 3 + ...ts.DynamicDataTests.DotNet9_0.verified.txt | 3 + .../Cache/BatchIfFixture.Overloads.cs | 73 +++++++++++++++++++ src/DynamicData.Tests/Cache/BatchIfFixture.cs | 8 +- .../Cache/ObservableCacheEx.BatchIf.cs | 12 +++ 5 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 src/DynamicData.Tests/Cache/BatchIfFixture.Overloads.cs diff --git a/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet8_0.verified.txt b/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet8_0.verified.txt index 92fa2bb0d..c89e20cfe 100644 --- a/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet8_0.verified.txt +++ b/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet8_0.verified.txt @@ -1156,6 +1156,9 @@ namespace DynamicData public static System.IObservable> BatchIf(this System.IObservable> source, System.IObservable pauseIfTrueSelector, System.Reactive.Concurrency.IScheduler? scheduler = null) where TObject : notnull where TKey : notnull { } + public static System.IObservable> BatchIf(this System.IObservable> source, System.IObservable pauseIfTrueSelector, System.IObservable? timer, System.Reactive.Concurrency.IScheduler? scheduler = null) + where TObject : notnull + where TKey : notnull { } public static System.IObservable> BatchIf(this System.IObservable> source, System.IObservable pauseIfTrueSelector, bool initialPauseState, System.Reactive.Concurrency.IScheduler? scheduler = null) where TObject : notnull where TKey : notnull { } diff --git a/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet9_0.verified.txt b/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet9_0.verified.txt index 5e852c6d4..2106d3127 100644 --- a/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet9_0.verified.txt +++ b/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet9_0.verified.txt @@ -1154,6 +1154,9 @@ namespace DynamicData public static System.IObservable> BatchIf(this System.IObservable> source, System.IObservable pauseIfTrueSelector, System.Reactive.Concurrency.IScheduler? scheduler = null) where TObject : notnull where TKey : notnull { } + public static System.IObservable> BatchIf(this System.IObservable> source, System.IObservable pauseIfTrueSelector, System.IObservable? timer, System.Reactive.Concurrency.IScheduler? scheduler = null) + where TObject : notnull + where TKey : notnull { } public static System.IObservable> BatchIf(this System.IObservable> source, System.IObservable pauseIfTrueSelector, System.TimeSpan? timeOut, System.Reactive.Concurrency.IScheduler? scheduler = null) where TObject : notnull where TKey : notnull { } diff --git a/src/DynamicData.Tests/Cache/BatchIfFixture.Overloads.cs b/src/DynamicData.Tests/Cache/BatchIfFixture.Overloads.cs new file mode 100644 index 000000000..c63484257 --- /dev/null +++ b/src/DynamicData.Tests/Cache/BatchIfFixture.Overloads.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2011-2026 Roland Pheasant. All rights reserved. +// Roland Pheasant licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.Reactive; +using System.Reactive.Subjects; + +using DynamicData.Tests.Domain; + +using FluentAssertions; + +using Xunit; + +using Randomizer = Bogus.Randomizer; + +namespace DynamicData.Tests.Cache; + +public partial class BatchIfFixture +{ + private const int OverloadSeed = 0x2409_1153; + + private readonly Randomizer _overloadRandomizer = new(OverloadSeed); + + /// Verifies that named timer overloads start without buffering unless a pause is requested. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void NamedTimerWithoutInitialState_StartsUnpaused(bool specifyScheduler) + { + // Arrange + using var pause = new Subject(); + using var timer = new Subject(); + var changes = _source.Connect(); + var batched = specifyScheduler + ? changes.BatchIf(pause, timer: timer, scheduler: _scheduler) + : changes.BatchIf(pause, timer: timer); + using var results = batched.AsAggregator(); + var person = new Person(_overloadRandomizer.String2(_overloadRandomizer.Int(5, 20)), _overloadRandomizer.Int(1, 100)); + + // Act + _source.AddOrUpdate(person); + + // Assert + results.Error.Should().BeNull(because: "a named timer is a supported overload shape"); + results.Data.Items.Should().Equal([person], because: "omitting the initial pause state means changes initially pass through"); + } + + /// Verifies that a named timer flushes changes accumulated during an explicit pause. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void NamedTimerWithoutInitialState_FlushesOnTimer(bool specifyScheduler) + { + // Arrange + using var pause = new Subject(); + using var timer = new Subject(); + var changes = _source.Connect(); + var batched = specifyScheduler + ? changes.BatchIf(pause, timer: timer, scheduler: _scheduler) + : changes.BatchIf(pause, timer: timer); + using var results = batched.AsAggregator(); + var person = new Person(_overloadRandomizer.String2(_overloadRandomizer.Int(5, 20)), _overloadRandomizer.Int(1, 100)); + pause.OnNext(true); + _source.AddOrUpdate(person); + + // Act + timer.OnNext(Unit.Default); + + // Assert + results.Error.Should().BeNull(because: "the timer must flush a valid buffered changeset"); + results.Data.Items.Should().Equal([person], because: "the timer overload must forward the accumulated item"); + } +} diff --git a/src/DynamicData.Tests/Cache/BatchIfFixture.cs b/src/DynamicData.Tests/Cache/BatchIfFixture.cs index d88bd2f03..d8f95c0a2 100644 --- a/src/DynamicData.Tests/Cache/BatchIfFixture.cs +++ b/src/DynamicData.Tests/Cache/BatchIfFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Reactive.Linq; using System.Reactive.Subjects; @@ -9,10 +9,11 @@ using Microsoft.Reactive.Testing; using Xunit; +using Xunit.Abstractions; namespace DynamicData.Tests.Cache; -public class BatchIfFixture : IDisposable +public partial class BatchIfFixture : IDisposable { private readonly ISubject _pausingSubject = new Subject(); @@ -22,8 +23,9 @@ public class BatchIfFixture : IDisposable private readonly ISourceCache _source; - public BatchIfFixture() + public BatchIfFixture(ITestOutputHelper output) { + output.WriteLine($"{nameof(BatchIfFixture)} seed: {OverloadSeed:X8}"); _scheduler = new TestScheduler(); _source = new SourceCache(p => p.Key); _results = _source.Connect().BatchIf(_pausingSubject, _scheduler).AsAggregator(); diff --git a/src/DynamicData/Cache/ObservableCacheEx.BatchIf.cs b/src/DynamicData/Cache/ObservableCacheEx.BatchIf.cs index e8733432c..c0abf0c66 100644 --- a/src/DynamicData/Cache/ObservableCacheEx.BatchIf.cs +++ b/src/DynamicData/Cache/ObservableCacheEx.BatchIf.cs @@ -37,6 +37,17 @@ public static IObservable> BatchIf(this where TObject : notnull where TKey : notnull => BatchIf(source, pauseIfTrueSelector, false, scheduler); + /// + /// The to conditionally buffer. + /// An that enables buffering when it emits . + /// An optional whose notifications flush buffered changes. + /// An optional for scheduling work. + /// This overload starts unpaused and delegates to the timer overload with initialPauseState: false. + /// + public static IObservable> BatchIf(this IObservable> source, IObservable pauseIfTrueSelector, IObservable? timer, IScheduler? scheduler = null) + where TObject : notnull + where TKey : notnull => BatchIf(source, pauseIfTrueSelector, false, timer, scheduler); + /// /// This overload delegates to the primary overload with default initialPauseState: false. public static IObservable> BatchIf(this IObservable> source, IObservable pauseIfTrueSelector, bool initialPauseState, IScheduler? scheduler = null) @@ -97,6 +108,7 @@ public static IObservable> BatchIf(this /// An optional timer. The buffer is flushed each time the timer produces a value, and buffering ceases when it completes. /// An optional for scheduling work. /// This overload accepts an explicit timer observable instead of a timeout. + /// public static IObservable> BatchIf(this IObservable> source, IObservable pauseIfTrueSelector, bool initialPauseState, IObservable? timer, IScheduler? scheduler = null) where TObject : notnull where TKey : notnull => new BatchIf(source, pauseIfTrueSelector, null, initialPauseState, timer, scheduler).Run(); From ab8183d798be466917c295e2daaa53dcfc2d1bba Mon Sep 17 00:00:00 2001 From: "Darrin W. Cullop" Date: Mon, 21 Sep 2026 07:32:26 -0700 Subject: [PATCH 2/2] Document BatchIf timer overload initial pause state --- .github/instructions/dynamicdata-cache.instructions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/instructions/dynamicdata-cache.instructions.md b/.github/instructions/dynamicdata-cache.instructions.md index 45b1874bb..526b6fd37 100644 --- a/.github/instructions/dynamicdata-cache.instructions.md +++ b/.github/instructions/dynamicdata-cache.instructions.md @@ -707,6 +707,8 @@ Watches a single key and emits `Optional` — `Some` when present, `Non Buffers changesets while a condition is true, flushes as a single combined changeset when condition becomes false. +The timer overload can omit `initialPauseState`, including calls with a named `timer` and optional `scheduler`. It starts unpaused. + | Input | Behavior | |-------|----------| | **Any (while paused)** | Buffered — combined into internal changeset list. |