Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/instructions/dynamicdata-cache.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,8 @@ Watches a single key and emits `Optional<TObject>` — `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. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1156,6 +1156,9 @@ namespace DynamicData
public static System.IObservable<DynamicData.IChangeSet<TObject, TKey>> BatchIf<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.IObservable<bool> pauseIfTrueSelector, System.Reactive.Concurrency.IScheduler? scheduler = null)
where TObject : notnull
where TKey : notnull { }
public static System.IObservable<DynamicData.IChangeSet<TObject, TKey>> BatchIf<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.IObservable<bool> pauseIfTrueSelector, System.IObservable<System.Reactive.Unit>? timer, System.Reactive.Concurrency.IScheduler? scheduler = null)
where TObject : notnull
where TKey : notnull { }
public static System.IObservable<DynamicData.IChangeSet<TObject, TKey>> BatchIf<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.IObservable<bool> pauseIfTrueSelector, bool initialPauseState, System.Reactive.Concurrency.IScheduler? scheduler = null)
where TObject : notnull
where TKey : notnull { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,9 @@ namespace DynamicData
public static System.IObservable<DynamicData.IChangeSet<TObject, TKey>> BatchIf<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.IObservable<bool> pauseIfTrueSelector, System.Reactive.Concurrency.IScheduler? scheduler = null)
where TObject : notnull
where TKey : notnull { }
public static System.IObservable<DynamicData.IChangeSet<TObject, TKey>> BatchIf<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.IObservable<bool> pauseIfTrueSelector, System.IObservable<System.Reactive.Unit>? timer, System.Reactive.Concurrency.IScheduler? scheduler = null)
where TObject : notnull
where TKey : notnull { }
public static System.IObservable<DynamicData.IChangeSet<TObject, TKey>> BatchIf<TObject, TKey>(this System.IObservable<DynamicData.IChangeSet<TObject, TKey>> source, System.IObservable<bool> pauseIfTrueSelector, System.TimeSpan? timeOut, System.Reactive.Concurrency.IScheduler? scheduler = null)
where TObject : notnull
where TKey : notnull { }
Expand Down
73 changes: 73 additions & 0 deletions src/DynamicData.Tests/Cache/BatchIfFixture.Overloads.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright (c) 2011-2026 Roland Pheasant. All rights reserved.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the rest of the codebase, we generally don't test operator overloads that are just a no-logic alias of another. I don't see why we should start here.

// 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);

/// <summary>Verifies that named timer overloads start without buffering unless a pause is requested.</summary>
[Theory]
[InlineData(false)]
[InlineData(true)]
public void NamedTimerWithoutInitialState_StartsUnpaused(bool specifyScheduler)
{
// Arrange
using var pause = new Subject<bool>();
using var timer = new Subject<Unit>();
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");
}

/// <summary>Verifies that a named timer flushes changes accumulated during an explicit pause.</summary>
[Theory]
[InlineData(false)]
[InlineData(true)]
public void NamedTimerWithoutInitialState_FlushesOnTimer(bool specifyScheduler)
{
// Arrange
using var pause = new Subject<bool>();
using var timer = new Subject<Unit>();
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");
}
}
8 changes: 5 additions & 3 deletions src/DynamicData.Tests/Cache/BatchIfFixture.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Reactive.Linq;
using System.Reactive.Subjects;

Expand All @@ -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<bool> _pausingSubject = new Subject<bool>();

Expand All @@ -22,8 +23,9 @@ public class BatchIfFixture : IDisposable

private readonly ISourceCache<Person, string> _source;

public BatchIfFixture()
public BatchIfFixture(ITestOutputHelper output)
{
output.WriteLine($"{nameof(BatchIfFixture)} seed: {OverloadSeed:X8}");
_scheduler = new TestScheduler();
_source = new SourceCache<Person, string>(p => p.Key);
_results = _source.Connect().BatchIf(_pausingSubject, _scheduler).AsAggregator();
Expand Down
12 changes: 12 additions & 0 deletions src/DynamicData/Cache/ObservableCacheEx.BatchIf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ public static IObservable<IChangeSet<TObject, TKey>> BatchIf<TObject, TKey>(this
where TObject : notnull
where TKey : notnull => BatchIf(source, pauseIfTrueSelector, false, scheduler);

/// <inheritdoc cref="BatchIf{TObject, TKey}(IObservable{IChangeSet{TObject, TKey}}, IObservable{bool}, bool, IObservable{Unit}, IScheduler)"/>
/// <param name="source">The <see cref="IObservable{IChangeSet{TObject, TKey}}"/> to conditionally buffer.</param>
/// <param name="pauseIfTrueSelector">An <see cref="IObservable{bool}"/> that enables buffering when it emits <see langword="true"/>.</param>
/// <param name="timer">An optional <see cref="IObservable{Unit}"/> whose notifications flush buffered changes.</param>
/// <param name="scheduler">An optional <see cref="IScheduler"/> for scheduling work.</param>
/// <remarks>This overload starts unpaused and delegates to the timer overload with <c>initialPauseState: false</c>.</remarks>
/// <seealso cref="BatchIf{TObject, TKey}(IObservable{IChangeSet{TObject, TKey}}, IObservable{bool}, bool, IObservable{Unit}, IScheduler)"/>
public static IObservable<IChangeSet<TObject, TKey>> BatchIf<TObject, TKey>(this IObservable<IChangeSet<TObject, TKey>> source, IObservable<bool> pauseIfTrueSelector, IObservable<Unit>? timer, IScheduler? scheduler = null)
where TObject : notnull
where TKey : notnull => BatchIf(source, pauseIfTrueSelector, false, timer, scheduler);

/// <inheritdoc cref="BatchIf{TObject, TKey}(IObservable{IChangeSet{TObject, TKey}}, IObservable{bool}, bool, TimeSpan?, IScheduler?)"/>
/// <remarks>This overload delegates to the primary overload with default <c>initialPauseState: false</c>.</remarks>
public static IObservable<IChangeSet<TObject, TKey>> BatchIf<TObject, TKey>(this IObservable<IChangeSet<TObject, TKey>> source, IObservable<bool> pauseIfTrueSelector, bool initialPauseState, IScheduler? scheduler = null)
Expand Down Expand Up @@ -97,6 +108,7 @@ public static IObservable<IChangeSet<TObject, TKey>> BatchIf<TObject, TKey>(this
/// <param name="timer">An optional <see cref="IObservable{Unit}"/> timer. The buffer is flushed each time the timer produces a value, and buffering ceases when it completes.</param>
/// <param name="scheduler">An optional <see cref="IScheduler"/> for scheduling work.</param>
/// <remarks>This overload accepts an explicit timer observable instead of a <see cref="TimeSpan"/> timeout.</remarks>
/// <seealso cref="BatchIf{TObject, TKey}(IObservable{IChangeSet{TObject, TKey}}, IObservable{bool}, IObservable{Unit}, IScheduler)"/>
public static IObservable<IChangeSet<TObject, TKey>> BatchIf<TObject, TKey>(this IObservable<IChangeSet<TObject, TKey>> source, IObservable<bool> pauseIfTrueSelector, bool initialPauseState, IObservable<Unit>? timer, IScheduler? scheduler = null)
where TObject : notnull
where TKey : notnull => new BatchIf<TObject, TKey>(source, pauseIfTrueSelector, null, initialPauseState, timer, scheduler).Run();
Expand Down
Loading