From 522d87d9358cc78ae4f80569bf497e33adef1e44 Mon Sep 17 00:00:00 2001 From: Guflly <145608489+Guflly@users.noreply.github.com> Date: Sat, 8 Aug 2026 00:35:20 -0700 Subject: [PATCH 1/3] Fix Switch resolution for virtualized changesets --- ...ts.DynamicDataTests.DotNet8_0.verified.txt | 3 + ...ts.DynamicDataTests.DotNet9_0.verified.txt | 3 + .../Cache/VirtualizedSwitchFixture.cs | 66 +++++++++++++++++++ .../Cache/ObservableCacheEx.Switch.cs | 11 ++++ 4 files changed, 83 insertions(+) create mode 100644 src/DynamicData.Tests/Cache/VirtualizedSwitchFixture.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..e0755b97d 100644 --- a/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet8_0.verified.txt +++ b/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet8_0.verified.txt @@ -1885,6 +1885,9 @@ namespace DynamicData public static System.IObservable> Switch(this System.IObservable>> sources) where TObject : notnull where TKey : notnull { } + public static System.IObservable> Switch(this System.IObservable>>> sources) + where TObject : notnull + where TKey : notnull { } public static System.IObservable> ToCollection(this System.IObservable> source) 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..fd9de1190 100644 --- a/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet9_0.verified.txt +++ b/src/DynamicData.Tests/API/ApiApprovalTests.DynamicDataTests.DotNet9_0.verified.txt @@ -1876,6 +1876,9 @@ namespace DynamicData public static System.IObservable> Switch(this System.IObservable>> sources) where TObject : notnull where TKey : notnull { } + public static System.IObservable> Switch(this System.IObservable>>> sources) + where TObject : notnull + where TKey : notnull { } public static System.IObservable> ToCollection(this System.IObservable> source) where TObject : notnull where TKey : notnull { } diff --git a/src/DynamicData.Tests/Cache/VirtualizedSwitchFixture.cs b/src/DynamicData.Tests/Cache/VirtualizedSwitchFixture.cs new file mode 100644 index 000000000..58843f1ae --- /dev/null +++ b/src/DynamicData.Tests/Cache/VirtualizedSwitchFixture.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using DynamicData; +using DynamicData.Tests; +using FluentAssertions; +using Xunit; + +namespace ExternalConsumerTests.Cache; + +public class VirtualizedSwitchFixture +{ + [Fact] + public void SwitchingAwayFromVirtualizedSourceRemovesItsItems() + { + using var enabled = new BehaviorSubject(true); + using var source = new SourceCache(value => value); + source.AddOrUpdate(Enumerable.Range(0, 20)); + + IObservable>> virtualized = CreateVirtualized(source, 10); + IObservable>>> sources = enabled.Select( + isEnabled => isEnabled ? virtualized : Observable.Empty>>()); + IObservable> switched = sources.Switch(); + using ChangeSetAggregator results = switched.AsAggregator(); + + results.Messages.Should().ContainSingle(); + results.Messages[0].Adds.Should().Be(10); + + enabled.OnNext(false); + + results.Messages.Should().HaveCount(2); + results.Messages[1].Removes.Should().Be(10); + results.Data.Items.Should().BeEmpty(); + } + + [Fact] + public void SwitchingBetweenVirtualizedSourcesReplacesVisibleItems() + { + using var first = new SourceCache(value => value); + using var second = new SourceCache(value => value); + first.AddOrUpdate(Enumerable.Range(0, 10)); + second.AddOrUpdate(Enumerable.Range(100, 10)); + + IObservable>> firstVirtualized = CreateVirtualized(first, 3); + IObservable>> secondVirtualized = CreateVirtualized(second, 3); + using var sources = new BehaviorSubject>>>(firstVirtualized); + IObservable> switched = sources.Switch(); + using ChangeSetAggregator results = switched.AsAggregator(); + + results.Data.Items.Should().BeEquivalentTo([0, 1, 2]); + + sources.OnNext(secondVirtualized); + + results.Messages.Should().HaveCount(3); + results.Messages[1].Removes.Should().Be(3); + results.Messages[2].Adds.Should().Be(3); + results.Data.Items.Should().BeEquivalentTo([100, 101, 102]); + } + + private static IObservable>> CreateVirtualized(SourceCache source, int size) => + source.Connect().SortAndVirtualize( + Comparer.Default, + Observable.Return(new VirtualRequest(0, size))); +} diff --git a/src/DynamicData/Cache/ObservableCacheEx.Switch.cs b/src/DynamicData/Cache/ObservableCacheEx.Switch.cs index 9c5d06c9e..c929fc1d9 100644 --- a/src/DynamicData/Cache/ObservableCacheEx.Switch.cs +++ b/src/DynamicData/Cache/ObservableCacheEx.Switch.cs @@ -37,6 +37,17 @@ public static IObservable> Switch(this return sources.Select(cache => cache.Connect()).Switch(); } + /// + /// An observable that emits virtualized changeset streams. + public static IObservable> Switch(this IObservable>>> sources) + where TObject : notnull + where TKey : notnull + { + sources.ThrowArgumentNullExceptionIfNull(nameof(sources)); + + return ObservableCacheEx.Switch((IObservable>>)sources); + } + /// /// Subscribes to the latest inner changeset stream, unsubscribing from the previous one on each switch. /// When switching, the old source's items are removed and the new source's items are added. From 0f5ffa78ff81614970a7b8c5201f31114693615d Mon Sep 17 00:00:00 2001 From: Guflly <145608489+Guflly@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:42:33 -0700 Subject: [PATCH 2/3] Remove redundant Switch null check --- src/DynamicData/Cache/ObservableCacheEx.Switch.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/DynamicData/Cache/ObservableCacheEx.Switch.cs b/src/DynamicData/Cache/ObservableCacheEx.Switch.cs index c929fc1d9..d8e997269 100644 --- a/src/DynamicData/Cache/ObservableCacheEx.Switch.cs +++ b/src/DynamicData/Cache/ObservableCacheEx.Switch.cs @@ -43,8 +43,6 @@ public static IObservable> Switch(this where TObject : notnull where TKey : notnull { - sources.ThrowArgumentNullExceptionIfNull(nameof(sources)); - return ObservableCacheEx.Switch((IObservable>>)sources); } From 1b9ea585d93bb7f65191d89c0b83a6dbe3e44208 Mon Sep 17 00:00:00 2001 From: Guflly <145608489+Guflly@users.noreply.github.com> Date: Sat, 8 Aug 2026 17:43:47 -0700 Subject: [PATCH 3/3] Use cache recorder in Switch tests --- .../Cache/VirtualizedSwitchFixture.cs | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/DynamicData.Tests/Cache/VirtualizedSwitchFixture.cs b/src/DynamicData.Tests/Cache/VirtualizedSwitchFixture.cs index 58843f1ae..35518263f 100644 --- a/src/DynamicData.Tests/Cache/VirtualizedSwitchFixture.cs +++ b/src/DynamicData.Tests/Cache/VirtualizedSwitchFixture.cs @@ -4,7 +4,7 @@ using System.Reactive.Linq; using System.Reactive.Subjects; using DynamicData; -using DynamicData.Tests; +using DynamicData.Tests.Utilities; using FluentAssertions; using Xunit; @@ -23,16 +23,19 @@ public void SwitchingAwayFromVirtualizedSourceRemovesItsItems() IObservable>>> sources = enabled.Select( isEnabled => isEnabled ? virtualized : Observable.Empty>>()); IObservable> switched = sources.Switch(); - using ChangeSetAggregator results = switched.AsAggregator(); + using var subscription = switched + .ValidateChangeSets(static value => value) + .RecordCacheItems(out var results); - results.Messages.Should().ContainSingle(); - results.Messages[0].Adds.Should().Be(10); + results.RecordedChangeSets.Should().ContainSingle(); + results.RecordedChangeSets[0].Adds.Should().Be(10); + results.RecordedItemsByKey.Values.Should().BeEquivalentTo(Enumerable.Range(0, 10)); enabled.OnNext(false); - results.Messages.Should().HaveCount(2); - results.Messages[1].Removes.Should().Be(10); - results.Data.Items.Should().BeEmpty(); + results.RecordedChangeSets.Should().HaveCount(2); + results.RecordedChangeSets[1].Removes.Should().Be(10); + results.RecordedItemsByKey.Should().BeEmpty(); } [Fact] @@ -47,16 +50,18 @@ public void SwitchingBetweenVirtualizedSourcesReplacesVisibleItems() IObservable>> secondVirtualized = CreateVirtualized(second, 3); using var sources = new BehaviorSubject>>>(firstVirtualized); IObservable> switched = sources.Switch(); - using ChangeSetAggregator results = switched.AsAggregator(); + using var subscription = switched + .ValidateChangeSets(static value => value) + .RecordCacheItems(out var results); - results.Data.Items.Should().BeEquivalentTo([0, 1, 2]); + results.RecordedItemsByKey.Values.Should().BeEquivalentTo([0, 1, 2]); sources.OnNext(secondVirtualized); - results.Messages.Should().HaveCount(3); - results.Messages[1].Removes.Should().Be(3); - results.Messages[2].Adds.Should().Be(3); - results.Data.Items.Should().BeEquivalentTo([100, 101, 102]); + results.RecordedChangeSets.Should().HaveCount(3); + results.RecordedChangeSets[1].Removes.Should().Be(3); + results.RecordedChangeSets[2].Adds.Should().Be(3); + results.RecordedItemsByKey.Values.Should().BeEquivalentTo([100, 101, 102]); } private static IObservable>> CreateVirtualized(SourceCache source, int size) =>