From 7585ae084d6d99ee86f98579c0d772ac696f8001 Mon Sep 17 00:00:00 2001 From: "Darrin W. Cullop" Date: Tue, 9 Jan 2024 12:57:02 -0800 Subject: [PATCH] Feature: Improve Performance of MergeManyChangeSets with fewer changesets (#829) * Reduce ChangeSets emitted by MergeManyChangeSets by using flag to determine if a parent changeset is currently being processed so that all changes initiated by a parent changeset are queued up until they've all been handled * Updated unit tests with new expected message count --- .../Cache/MergeManyChangeSetsCacheFixture.cs | 26 ++++++- ...ManyChangeSetsCacheSourceCompareFixture.cs | 6 +- .../Cache/MergeManyChangeSetsListFixture.cs | 26 +++---- .../List/MergeManyChangeSetsCacheFixture.cs | 4 +- .../List/MergeManyChangeSetsListFixture.cs | 37 +++++----- .../Cache/Internal/ChangeSetMergeTracker.cs | 23 ++++-- .../Internal/MergeManyCacheChangeSets.cs | 16 +++-- .../MergeManyCacheChangeSetsSourceCompare.cs | 34 ++++++--- .../Cache/Internal/MergeManyListChangeSets.cs | 66 +++++++++-------- src/DynamicData/Internal/ObservableEx.cs | 13 ++++ .../List/Internal/ChangeSetMergeTracker.cs | 33 +++++---- .../List/Internal/MergeManyCacheChangeSets.cs | 71 ++++++++++--------- .../List/Internal/MergeManyListChangeSets.cs | 67 +++++++++-------- 13 files changed, 257 insertions(+), 165 deletions(-) create mode 100644 src/DynamicData/Internal/ObservableEx.cs diff --git a/src/DynamicData.Tests/Cache/MergeManyChangeSetsCacheFixture.cs b/src/DynamicData.Tests/Cache/MergeManyChangeSetsCacheFixture.cs index a2abcc4f5..58c6c189e 100644 --- a/src/DynamicData.Tests/Cache/MergeManyChangeSetsCacheFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeManyChangeSetsCacheFixture.cs @@ -230,7 +230,7 @@ public void AllExistingSubItemsPresentInResult() _marketCacheResults.Data.Count.Should().Be(MarketCount); markets.Sum(m => m.PricesCache.Count).Should().Be(MarketCount * PricesPerMarket); results.Data.Count.Should().Be(MarketCount * PricesPerMarket); - results.Messages.Count.Should().Be(MarketCount); + results.Messages.Count.Should().Be(1); results.Summary.Overall.Adds.Should().Be(MarketCount * PricesPerMarket); results.Summary.Overall.Removes.Should().Be(0); results.Summary.Overall.Updates.Should().Be(0); @@ -390,19 +390,41 @@ public void AnySourceItemRemovedRemovesAllSourceValues() // having var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); - _marketCache.AddOrUpdate(markets); AddUniquePrices(markets); + _marketCache.AddOrUpdate(markets); // when _marketCache.Edit(updater => updater.RemoveKeys(updater.Keys.Take(RemoveCount))); // then _marketCacheResults.Data.Count.Should().Be(MarketCount - RemoveCount); + results.Messages.Count.Should().Be(2); results.Data.Count.Should().Be((MarketCount - RemoveCount) * PricesPerMarket); results.Summary.Overall.Adds.Should().Be(MarketCount * PricesPerMarket); results.Summary.Overall.Removes.Should().Be(PricesPerMarket * RemoveCount); } + [Fact] + public void ClearingParentEmitsSingleChangeSet() + { + // having + var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); + using var results = _marketCache.Connect().MergeManyChangeSets(m => m.LatestPrices, MarketPrice.EqualityComparer).AsAggregator(); + AddUniquePrices(markets); + _marketCache.AddOrUpdate(markets); + + // when + _marketCache.Clear(); + + // then + _marketCacheResults.Data.Count.Should().Be(0); + results.Data.Count.Should().Be(0); + results.Messages.Count.Should().Be(2); + results.Summary.Overall.Adds.Should().Be(MarketCount * PricesPerMarket); + results.Summary.Overall.Removes.Should().Be(MarketCount * PricesPerMarket); + results.Summary.Overall.Updates.Should().Be(0); + } + [Fact] public void ChangingSourceByUpdateRemovesPreviousAndAddsNewValues() { diff --git a/src/DynamicData.Tests/Cache/MergeManyChangeSetsCacheSourceCompareFixture.cs b/src/DynamicData.Tests/Cache/MergeManyChangeSetsCacheSourceCompareFixture.cs index 60f27a4c7..54683c1c0 100644 --- a/src/DynamicData.Tests/Cache/MergeManyChangeSetsCacheSourceCompareFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeManyChangeSetsCacheSourceCompareFixture.cs @@ -228,7 +228,7 @@ public void AllExistingSubItemsPresentInResult() _marketCacheResults.Data.Count.Should().Be(MarketCount); markets.Sum(m => m.PricesCache.Count).Should().Be(MarketCount * PricesPerMarket); results.Data.Count.Should().Be(MarketCount * PricesPerMarket); - results.Messages.Count.Should().Be(MarketCount); + results.Messages.Count.Should().Be(1); results.Summary.Overall.Adds.Should().Be(MarketCount * PricesPerMarket); results.Summary.Overall.Removes.Should().Be(0); results.Summary.Overall.Updates.Should().Be(0); @@ -263,8 +263,8 @@ public void AllRefreshedSubItemsAreRefreshed() // having var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray(); using var results = ChangeSetByRating().AsAggregator(); - _marketCache.AddOrUpdate(markets); markets.Select((m, index) => new { Market = m, Index = index }).ForEach(m => m.Market.SetPrices(m.Index * ItemIdStride, (m.Index * ItemIdStride) + PricesPerMarket, GetRandomPrice)); + _marketCache.AddOrUpdate(markets); // when markets.ForEach(m => m.RefreshAllPrices(GetRandomPrice)); @@ -272,7 +272,7 @@ public void AllRefreshedSubItemsAreRefreshed() // then _marketCacheResults.Data.Count.Should().Be(MarketCount); results.Data.Count.Should().Be(MarketCount * PricesPerMarket); - results.Messages.Count.Should().Be(MarketCount * 2); + results.Messages.Count.Should().Be(MarketCount + 1); results.Summary.Overall.Adds.Should().Be(MarketCount * PricesPerMarket); results.Summary.Overall.Removes.Should().Be(0); results.Summary.Overall.Updates.Should().Be(0); diff --git a/src/DynamicData.Tests/Cache/MergeManyChangeSetsListFixture.cs b/src/DynamicData.Tests/Cache/MergeManyChangeSetsListFixture.cs index 6211e141e..b6c82e438 100644 --- a/src/DynamicData.Tests/Cache/MergeManyChangeSetsListFixture.cs +++ b/src/DynamicData.Tests/Cache/MergeManyChangeSetsListFixture.cs @@ -161,7 +161,7 @@ public void ResultContainsAllInitialChildren() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount); + _animalResults.Messages.Count.Should().Be(1); CheckResultContents(); } @@ -176,7 +176,7 @@ public void ResultContainsChildrenFromAddedParents() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount + 1); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); addThis.Animals.Items.ForEach(added => _animalResults.Data.Items.Should().Contain(added)); CheckResultContents(); } @@ -192,7 +192,7 @@ public void ResultDoesNotContainChildrenFromParentsRemovedWithRemove() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount - 1); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); removeThis.Animals.Items.ForEach(removed => _animalResults.Data.Items.Should().NotContain(removed)); CheckResultContents(); removeThis.Dispose(); @@ -209,7 +209,7 @@ public void ResultDoesNotContainChildrenFromParentsBatchRemoved() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount - RemoveRangeSize); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + RemoveRangeSize); + _animalResults.Messages.Count.Should().Be(2); removeThese.SelectMany(owner => owner.Animals.Items).ForEach(removed => _animalResults.Data.Items.Should().NotContain(removed)); CheckResultContents(); removeThese.ForEach(owner => owner.Dispose()); @@ -227,7 +227,7 @@ public void ResultContainsCorrectItemsAfterParentUpdate() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); // Owner Count should not change - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 2); // +2 = 1 Message removing animals from old value, +1 message adding from new value + _animalResults.Messages.Count.Should().Be(2); // 2 = Initial Add and one changeset with remove old items / add new items replaceThis.Animals.Items.ForEach(removed => _animalResults.Data.Items.Should().NotContain(removed)); withThis.Animals.Items.ForEach(added => _animalResults.Data.Items.Should().Contain(added)); CheckResultContents(); @@ -262,7 +262,7 @@ public void ResultContainsChildrenAddedWithAddRange() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount * 2); + _animalResults.Messages.Count.Should().Be(1 + InitialOwnerCount); // Initial + 1 for each Range Added totalAdded.ForEach(animal => _animalResults.Data.Items.Should().Contain(animal)); _animalOwners.Items.Sum(owner => owner.Animals.Count).Should().Be(initialCount + totalAdded.Count); CheckResultContents(); @@ -283,7 +283,7 @@ public void ResultContainsChildrenAddedWithInsert() // Assert randomOwner.Animals.Items.ElementAt(insertIndex).Should().Be(insertThis); _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); _animalResults.Data.Items.Should().Contain(insertThis); _animalOwners.Items.Sum(owner => owner.Animals.Count).Should().Be(initialCount + 1); CheckResultContents(); @@ -302,7 +302,7 @@ public void ResultDoesNotContainChildrenRemovedWithRemove() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); _animalResults.Data.Items.Should().NotContain(removeThis); _animalOwners.Items.Sum(owner => owner.Animals.Count).Should().Be(initialCount - 1); CheckResultContents(); @@ -322,7 +322,7 @@ public void ResultDoesNotContainChildrenRemovedWithRemoveAt() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); _animalResults.Data.Items.Should().NotContain(removeThis); _animalOwners.Items.Sum(owner => owner.Animals.Count).Should().Be(initialCount - 1); CheckResultContents(); @@ -342,7 +342,7 @@ public void ResultDoesNotContainChildrenRemovedWithRemoveRange() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); removeThese.ForEach(removed => randomOwner.Animals.Items.Should().NotContain(removed)); CheckResultContents(); } @@ -360,7 +360,7 @@ public void ResultDoesNotContainChildrenRemovedWithRemoveMany() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); removeThese.ForEach(removed => randomOwner.Animals.Items.Should().NotContain(removed)); CheckResultContents(); } @@ -378,7 +378,7 @@ public void ResultContainsCorrectItemsAfterChildReplacement() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); randomOwner.Animals.Items.Should().NotContain(replaceThis); randomOwner.Animals.Items.Should().Contain(withThis); CheckResultContents(); @@ -396,7 +396,7 @@ public void ResultContainsCorrectItemsAfterChildClear() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); randomOwner.Animals.Count.Should().Be(0); removedAnimals.ForEach(removed => _animalResults.Data.Items.Should().NotContain(removed)); CheckResultContents(); diff --git a/src/DynamicData.Tests/List/MergeManyChangeSetsCacheFixture.cs b/src/DynamicData.Tests/List/MergeManyChangeSetsCacheFixture.cs index 91313f5b2..d2b859deb 100644 --- a/src/DynamicData.Tests/List/MergeManyChangeSetsCacheFixture.cs +++ b/src/DynamicData.Tests/List/MergeManyChangeSetsCacheFixture.cs @@ -198,7 +198,7 @@ public void AllExistingSubItemsPresentInResult() _marketListResults.Data.Count.Should().Be(MarketCount); markets.Sum(m => m.PricesCache.Count).Should().Be(MarketCount * PricesPerMarket); results.Data.Count.Should().Be(MarketCount * PricesPerMarket); - results.Messages.Count.Should().Be(MarketCount); + results.Messages.Count.Should().Be(1); results.Summary.Overall.Adds.Should().Be(MarketCount * PricesPerMarket); results.Summary.Overall.Removes.Should().Be(0); results.Summary.Overall.Updates.Should().Be(0); @@ -824,8 +824,6 @@ public void Dispose() DisposeMarkets(); } - private void AddUniquePrices(Market[] markets) => markets.ForEach(m => m.AddUniquePrices(PricesPerMarket, _ => GetRandomPrice())); - private void CheckResultContents(ChangeSetAggregator marketResults, ChangeSetAggregator priceResults) { var expectedMarkets = _marketList.Items.ToList(); diff --git a/src/DynamicData.Tests/List/MergeManyChangeSetsListFixture.cs b/src/DynamicData.Tests/List/MergeManyChangeSetsListFixture.cs index 3fd868bf6..8abc7dcdb 100644 --- a/src/DynamicData.Tests/List/MergeManyChangeSetsListFixture.cs +++ b/src/DynamicData.Tests/List/MergeManyChangeSetsListFixture.cs @@ -150,7 +150,7 @@ public void ResultContainsAllInitialChildren() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount); + _animalResults.Messages.Count.Should().Be(1); CheckResultContents(); } @@ -165,7 +165,7 @@ public void ResultContainsChildrenFromParentsAddedWithAddRange() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount + AddRangeSize); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + AddRangeSize); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for additional add addThese.SelectMany(added => added.Animals.Items).ForEach(added => _animalResults.Data.Items.Should().Contain(added)); CheckResultContents(); } @@ -181,7 +181,7 @@ public void ResultContainsChildrenFromParentsAddedWithAdd() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount + 1); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for additional add addThis.Animals.Items.ForEach(added => _animalResults.Data.Items.Should().Contain(added)); CheckResultContents(); } @@ -199,7 +199,7 @@ public void ResultContainsChildrenFromParentsAddedWithInsert() // Assert _animalOwners.Items.ElementAt(insertIndex).Should().Be(insertThis); _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount + 1); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for additional add insertThis.Animals.Items.ForEach(added => _animalResults.Data.Items.Should().Contain(added)); CheckResultContents(); } @@ -215,7 +215,7 @@ public void ResultDoesNotContainChildrenFromParentsRemovedWithRemove() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount - 1); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for removing removeThis.Animals.Items.ForEach(removed => _animalResults.Data.Items.Should().NotContain(removed)); CheckResultContents(); removeThis.Dispose(); @@ -233,7 +233,7 @@ public void ResultDoesNotContainChildrenFromParentsRemovedWithRemoveAt() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount - 1); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for removing removeThis.Animals.Items.ForEach(removed => _animalResults.Data.Items.Should().NotContain(removed)); CheckResultContents(); removeThis.Dispose(); @@ -251,7 +251,7 @@ public void ResultDoesNotContainChildrenFromParentsRemovedWithRemoveRange() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount - RemoveRangeSize); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + RemoveRangeSize); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for removing removeThese.SelectMany(owner => owner.Animals.Items).ForEach(removed => _animalResults.Data.Items.Should().NotContain(removed)); CheckResultContents(); removeThese.ForEach(owner => owner.Dispose()); @@ -268,7 +268,7 @@ public void ResultDoesNotContainChildrenFromParentsRemovedWithRemoveMany() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount - RemoveRangeSize); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + RemoveRangeSize); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for removing removeThese.SelectMany(owner => owner.Animals.Items).ForEach(removed => _animalResults.Data.Items.Should().NotContain(removed)); CheckResultContents(); removeThese.ForEach(owner => owner.Dispose()); @@ -286,7 +286,7 @@ public void ResultContainsCorrectItemsAfterParentReplacement() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); // Owner Count should not change - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 2); // +2 = 1 Message removing animals from old value, +1 message adding from new value + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for removing replaceThis.Animals.Items.ForEach(removed => _animalResults.Data.Items.Should().NotContain(removed)); withThis.Animals.Items.ForEach(added => _animalResults.Data.Items.Should().Contain(added)); CheckResultContents(); @@ -305,6 +305,7 @@ public void ResultEmptyIfSourceIsCleared() // Assert _animalOwnerResults.Data.Count.Should().Be(0); _animalResults.Data.Count.Should().Be(0); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for removing CheckResultContents(); items.ForEach(owner => owner.Dispose()); } @@ -322,7 +323,7 @@ public void ResultContainsChildrenAddedWithAddRange() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for additional add addThese.ForEach(animal => _animalResults.Data.Items.Should().Contain(animal)); _animalOwners.Items.Sum(owner => owner.Animals.Count).Should().Be(initialCount + AddRangeSize); CheckResultContents(); @@ -341,7 +342,7 @@ public void ResultContainsChildrenAddedWithAdd() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for removing _animalResults.Data.Items.Should().Contain(addThis); _animalOwners.Items.Sum(owner => owner.Animals.Count).Should().Be(initialCount + 1); CheckResultContents(); @@ -362,7 +363,7 @@ public void ResultContainsChildrenAddedWithInsert() // Assert randomOwner.Animals.Items.ElementAt(insertIndex).Should().Be(insertThis); _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for additional add _animalResults.Data.Items.Should().Contain(insertThis); _animalOwners.Items.Sum(owner => owner.Animals.Count).Should().Be(initialCount + 1); CheckResultContents(); @@ -381,7 +382,7 @@ public void ResultDoesNotContainChildrenRemovedWithRemove() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for removing _animalResults.Data.Items.Should().NotContain(removeThis); _animalOwners.Items.Sum(owner => owner.Animals.Count).Should().Be(initialCount - 1); CheckResultContents(); @@ -401,7 +402,7 @@ public void ResultDoesNotContainChildrenRemovedWithRemoveAt() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for removing _animalResults.Data.Items.Should().NotContain(removeThis); _animalOwners.Items.Sum(owner => owner.Animals.Count).Should().Be(initialCount - 1); CheckResultContents(); @@ -421,7 +422,7 @@ public void ResultDoesNotContainChildrenRemovedWithRemoveRange() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for removing removeThese.ForEach(removed => randomOwner.Animals.Items.Should().NotContain(removed)); CheckResultContents(); } @@ -439,7 +440,7 @@ public void ResultDoesNotContainChildrenRemovedWithRemoveMany() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for removing removeThese.ForEach(removed => randomOwner.Animals.Items.Should().NotContain(removed)); CheckResultContents(); } @@ -457,7 +458,7 @@ public void ResultContainsCorrectItemsAfterChildReplacement() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for update randomOwner.Animals.Items.Should().NotContain(replaceThis); randomOwner.Animals.Items.Should().Contain(withThis); CheckResultContents(); @@ -475,7 +476,7 @@ public void ResultContainsCorrectItemsAfterChildClear() // Assert _animalOwnerResults.Data.Count.Should().Be(InitialOwnerCount); - _animalResults.Messages.Count.Should().Be(InitialOwnerCount + 1); + _animalResults.Messages.Count.Should().Be(2); // 1 for initial add, 1 for removing randomOwner.Animals.Count.Should().Be(0); removedAnimals.ForEach(removed => _animalResults.Data.Items.Should().NotContain(removed)); CheckResultContents(); diff --git a/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs b/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs index ecd2bba98..c720bbebb 100644 --- a/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs +++ b/src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs @@ -13,7 +13,7 @@ internal sealed class ChangeSetMergeTracker(Func _resultCache = new(); - public void RemoveItems(IEnumerable> items, IObserver> observer) + public void RemoveItems(IEnumerable> items, IObserver>? observer = null) { var sourceCaches = selectCaches().ToArray(); @@ -34,10 +34,13 @@ public void RemoveItems(IEnumerable> items, IObserve } } - EmitChanges(observer); + if (observer != null) + { + EmitChanges(observer); + } } - public void RefreshItems(IEnumerable keys, IObserver> observer) + public void RefreshItems(IEnumerable keys, IObserver>? observer = null) { var sourceCaches = selectCaches().ToArray(); @@ -58,10 +61,13 @@ public void RefreshItems(IEnumerable keys, IObserver changes, IObserver> observer) + public void ProcessChangeSet(IChangeSet changes, IObserver>? observer = null) { var sourceCaches = selectCaches().ToArray(); @@ -87,10 +93,13 @@ public void ProcessChangeSet(IChangeSet changes, IObserver> observer) + public void EmitChanges(IObserver> observer) { var changeSet = _resultCache.CaptureChanges(); if (changeSet.Count != 0) diff --git a/src/DynamicData/Cache/Internal/MergeManyCacheChangeSets.cs b/src/DynamicData/Cache/Internal/MergeManyCacheChangeSets.cs index fdfed529e..23da5a000 100644 --- a/src/DynamicData/Cache/Internal/MergeManyCacheChangeSets.cs +++ b/src/DynamicData/Cache/Internal/MergeManyCacheChangeSets.cs @@ -4,6 +4,7 @@ using System.Reactive.Disposables; using System.Reactive.Linq; +using DynamicData.Internal; namespace DynamicData.Cache.Internal; @@ -21,6 +22,7 @@ public IObservable> Run() => Observabl { var locker = new object(); var cache = new Cache, TKey>(); + var parentUpdate = false; // This is manages all of the changes var changeTracker = new ChangeSetMergeTracker(() => cache.Items, comparer, equalityComparer); @@ -30,20 +32,26 @@ public IObservable> Run() => Observabl .Transform((obj, key) => new ChangeSetCache(selector(obj, key).Synchronize(locker))) .Synchronize(locker) .Do(cache.Clone) + .Do(_ => parentUpdate = true) .Publish(); // Merge the child changeset changes together and apply to the tracker var subMergeMany = shared .MergeMany(cacheChangeSet => cacheChangeSet.Source) - .Subscribe( - changes => changeTracker.ProcessChangeSet(changes, observer), + .SubscribeSafe( + changes => changeTracker.ProcessChangeSet(changes, !parentUpdate ? observer : null), observer.OnError, observer.OnCompleted); // When a source item is removed, all of its sub-items need to be removed var subRemove = shared - .OnItemRemoved(changeSetCache => changeTracker.RemoveItems(changeSetCache.Cache.KeyValues, observer), invokeOnUnsubscribe: false) - .OnItemUpdated((_, prev) => changeTracker.RemoveItems(prev.Cache.KeyValues, observer)) + .OnItemRemoved(changeSetCache => changeTracker.RemoveItems(changeSetCache.Cache.KeyValues), invokeOnUnsubscribe: false) + .OnItemUpdated((_, prev) => changeTracker.RemoveItems(prev.Cache.KeyValues)) + .Do(_ => + { + changeTracker.EmitChanges(observer); + parentUpdate = false; + }) .Subscribe(); return new CompositeDisposable(shared.Connect(), subMergeMany, subRemove); diff --git a/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs b/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs index 4dc3a4272..c7385735a 100644 --- a/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs +++ b/src/DynamicData/Cache/Internal/MergeManyCacheChangeSetsSourceCompare.cs @@ -4,6 +4,7 @@ using System.Reactive.Disposables; using System.Reactive.Linq; +using DynamicData.Internal; namespace DynamicData.Cache.Internal; @@ -28,6 +29,7 @@ public IObservable> Run() => Observabl { var locker = new object(); var cache = new Cache, TKey>(); + var parentUpdate = false; // This is manages all of the changes var changeTracker = new ChangeSetMergeTracker(() => cache.Items, _comparer, _equalityComparer); @@ -37,28 +39,38 @@ public IObservable> Run() => Observabl .Transform((obj, key) => new ChangeSetCache(_changeSetSelector(obj, key).Synchronize(locker))) .Synchronize(locker) .Do(cache.Clone) + .Do(_ => parentUpdate = true) .Publish(); // Merge the child changeset changes together and apply to the tracker var subMergeMany = shared .MergeMany(changeSetCache => changeSetCache.Source) - .Subscribe( - changes => changeTracker.ProcessChangeSet(changes, observer), + .SubscribeSafe( + changes => changeTracker.ProcessChangeSet(changes, !parentUpdate ? observer : null), observer.OnError, observer.OnCompleted); // When a source item is removed, all of its sub-items need to be removed - var subRemove = shared - .OnItemRemoved(cacheChangeSet => changeTracker.RemoveItems(cacheChangeSet.Cache.KeyValues, observer), invokeOnUnsubscribe: false) - .OnItemUpdated((_, prev) => changeTracker.RemoveItems(prev.Cache.KeyValues, observer)) + var parentObservable = shared + .OnItemRemoved(cacheChangeSet => changeTracker.RemoveItems(cacheChangeSet.Cache.KeyValues), invokeOnUnsubscribe: false) + .OnItemUpdated((_, prev) => changeTracker.RemoveItems(prev.Cache.KeyValues)); + + // If requested, handle refresh events as well + if (reevalOnRefresh) + { + parentObservable = parentObservable.OnItemRefreshed(cacheChangeSet => changeTracker.RefreshItems(cacheChangeSet.Cache.Keys)); + } + + // Subscribe to handle all the requested changes and emit them downstream + var subParent = parentObservable + .Do(_ => + { + changeTracker.EmitChanges(observer); + parentUpdate = false; + }) .Subscribe(); - // Optionally attach a handler for Refresh events - var subRefresh = reevalOnRefresh - ? shared.OnItemRefreshed(cacheChangeSet => changeTracker.RefreshItems(cacheChangeSet.Cache.Keys, observer)).Subscribe() - : Disposable.Empty; - - return new CompositeDisposable(shared.Connect(), subMergeMany, subRemove, subRefresh); + return new CompositeDisposable(shared.Connect(), subMergeMany, subParent); }).Select(changes => changes.Transform(entry => entry.Child)); private sealed class ParentChildEntry(TObject parent, TDestination child) diff --git a/src/DynamicData/Cache/Internal/MergeManyListChangeSets.cs b/src/DynamicData/Cache/Internal/MergeManyListChangeSets.cs index e50197fa1..c9949868a 100644 --- a/src/DynamicData/Cache/Internal/MergeManyListChangeSets.cs +++ b/src/DynamicData/Cache/Internal/MergeManyListChangeSets.cs @@ -4,6 +4,7 @@ using System.Reactive.Disposables; using System.Reactive.Linq; +using DynamicData.Internal; using DynamicData.List.Internal; namespace DynamicData.Cache.Internal; @@ -17,33 +18,40 @@ internal sealed class MergeManyListChangeSets(IObse where TDestination : notnull { public IObservable> Run() => Observable.Create>( - observer => - { - var locker = new object(); - - // This is manages all of the changes - var changeTracker = new ChangeSetMergeTracker(); - - // Transform to a cache changeset of child lists, synchronize, and publish. - var shared = source - .Transform((obj, key) => new ClonedListChangeSet(selector(obj, key).Synchronize(locker), equalityComparer)) - .Synchronize(locker) - .Publish(); - - // Merge the child changeset changes together and apply to the tracker - var subMergeMany = shared - .MergeMany(clonedList => clonedList.Source.RemoveIndex()) - .Subscribe( - changes => changeTracker.ProcessChangeSet(changes, observer), - observer.OnError, - observer.OnCompleted); - - // When a source item is removed, all of its sub-items need to be removed - var subRemove = shared - .OnItemRemoved(clonedList => changeTracker.RemoveItems(clonedList.List, observer), invokeOnUnsubscribe: false) - .OnItemUpdated((_, prev) => changeTracker.RemoveItems(prev.List, observer)) - .Subscribe(); - - return new CompositeDisposable(shared.Connect(), subMergeMany, subRemove); - }); + observer => + { + var locker = new object(); + var parentUpdate = false; + + // This is manages all of the changes + var changeTracker = new ChangeSetMergeTracker(); + + // Transform to a cache changeset of child lists, synchronize, and publish. + var shared = source + .Transform((obj, key) => new ClonedListChangeSet(selector(obj, key).Synchronize(locker), equalityComparer)) + .Synchronize(locker) + .Do(_ => parentUpdate = true) + .Publish(); + + // Merge the child changeset changes together and apply to the tracker + var subMergeMany = shared + .MergeMany(clonedList => clonedList.Source.RemoveIndex()) + .SubscribeSafe( + changes => changeTracker.ProcessChangeSet(changes, !parentUpdate ? observer : null), + observer.OnError, + observer.OnCompleted); + + // When a source item is removed, all of its sub-items need to be removed + var subRemove = shared + .OnItemRemoved(clonedList => changeTracker.RemoveItems(clonedList.List), invokeOnUnsubscribe: false) + .OnItemUpdated((_, prev) => changeTracker.RemoveItems(prev.List)) + .Do(_ => + { + changeTracker.EmitChanges(observer); + parentUpdate = false; + }) + .Subscribe(); + + return new CompositeDisposable(shared.Connect(), subMergeMany, subRemove); + }); } diff --git a/src/DynamicData/Internal/ObservableEx.cs b/src/DynamicData/Internal/ObservableEx.cs new file mode 100644 index 000000000..0d4555b8f --- /dev/null +++ b/src/DynamicData/Internal/ObservableEx.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2011-2023 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; + +namespace DynamicData.Internal; + +internal static class ObservableEx +{ + public static IDisposable SubscribeSafe(this IObservable observable, Action onNext, Action onError, Action onComplete) => + observable.SubscribeSafe(Observer.Create(onNext, onError, onComplete)); +} diff --git a/src/DynamicData/List/Internal/ChangeSetMergeTracker.cs b/src/DynamicData/List/Internal/ChangeSetMergeTracker.cs index b518116be..b9233fb4f 100644 --- a/src/DynamicData/List/Internal/ChangeSetMergeTracker.cs +++ b/src/DynamicData/List/Internal/ChangeSetMergeTracker.cs @@ -9,7 +9,7 @@ internal sealed class ChangeSetMergeTracker { private readonly ChangeAwareList _resultList = new(); - public void ProcessChangeSet(IChangeSet changes, IObserver> observer) + public void ProcessChangeSet(IChangeSet changes, IObserver>? observer = null) { foreach (var change in changes) { @@ -49,13 +49,29 @@ public void ProcessChangeSet(IChangeSet changes, IObserver removeItems, IObserver> observer) + public void RemoveItems(IEnumerable removeItems, IObserver>? observer = null) { _resultList.Remove(removeItems); - EmitChanges(observer); + + if (observer != null) + { + EmitChanges(observer); + } + } + + public void EmitChanges(IObserver> observer) + { + var changeSet = _resultList.CaptureChanges(); + if (changeSet.Count != 0) + { + observer.OnNext(changeSet); + } } private void OnClear(Change change) => _resultList.ClearOrRemoveMany(change); @@ -71,13 +87,4 @@ public void RemoveItems(IEnumerable removeItems, IObserver range) => _resultList.AddRange(range); private void OnRangeRemoved(RangeChange range) => _resultList.Remove(range); - - private void EmitChanges(IObserver> observer) - { - var changeSet = _resultList.CaptureChanges(); - if (changeSet.Count != 0) - { - observer.OnNext(changeSet); - } - } } diff --git a/src/DynamicData/List/Internal/MergeManyCacheChangeSets.cs b/src/DynamicData/List/Internal/MergeManyCacheChangeSets.cs index 0de3c5afd..074443613 100644 --- a/src/DynamicData/List/Internal/MergeManyCacheChangeSets.cs +++ b/src/DynamicData/List/Internal/MergeManyCacheChangeSets.cs @@ -5,6 +5,7 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using DynamicData.Cache.Internal; +using DynamicData.Internal; namespace DynamicData.List.Internal; @@ -16,36 +17,42 @@ internal sealed class MergeManyCacheChangeSets> Run() => - Observable.Create>( - observer => - { - var locker = new object(); - var list = new List>(); - - // This is manages all of the changes - var changeTracker = new ChangeSetMergeTracker(() => list, comparer, equalityComparer); - - // Transform to a list changeset of child caches, synchronize, update the local copy, and publish. - var shared = source - .Transform(obj => new ChangeSetCache(changeSetSelector(obj).Synchronize(locker))) - .Synchronize(locker) - .Do(list.Clone) - .Publish(); - - // Merge the child changeset changes together and apply to the tracker - var subMergeMany = shared - .MergeMany(chanceSetCache => chanceSetCache.Source) - .Subscribe( - changes => changeTracker.ProcessChangeSet(changes, observer), - observer.OnError, - observer.OnCompleted); - - // When a source item is removed, all of its sub-items need to be removed - var subRemove = shared - .OnItemRemoved(changeSetCache => changeTracker.RemoveItems(changeSetCache.Cache.KeyValues, observer), invokeOnUnsubscribe: false) - .Subscribe(); - - return new CompositeDisposable(shared.Connect(), subMergeMany, subRemove); - }); + public IObservable> Run() => Observable.Create>( + observer => + { + var locker = new object(); + var list = new List>(); + var parentUpdate = false; + + // This is manages all of the changes + var changeTracker = new ChangeSetMergeTracker(() => list, comparer, equalityComparer); + + // Transform to a list changeset of child caches, synchronize, update the local copy, and publish. + var shared = source + .Transform(obj => new ChangeSetCache(changeSetSelector(obj).Synchronize(locker))) + .Synchronize(locker) + .Do(list.Clone) + .Do(_ => parentUpdate = true) + .Publish(); + + // Merge the child changeset changes together and apply to the tracker + var subMergeMany = shared + .MergeMany(chanceSetCache => chanceSetCache.Source) + .SubscribeSafe( + changes => changeTracker.ProcessChangeSet(changes, !parentUpdate ? observer : null), + observer.OnError, + observer.OnCompleted); + + // When a source item is removed, all of its sub-items need to be removed + var subRemove = shared + .OnItemRemoved(changeSetCache => changeTracker.RemoveItems(changeSetCache.Cache.KeyValues), invokeOnUnsubscribe: false) + .Do(_ => + { + changeTracker.EmitChanges(observer); + parentUpdate = false; + }) + .Subscribe(); + + return new CompositeDisposable(shared.Connect(), subMergeMany, subRemove); + }); } diff --git a/src/DynamicData/List/Internal/MergeManyListChangeSets.cs b/src/DynamicData/List/Internal/MergeManyListChangeSets.cs index dd2190f20..b7a3a4fe7 100644 --- a/src/DynamicData/List/Internal/MergeManyListChangeSets.cs +++ b/src/DynamicData/List/Internal/MergeManyListChangeSets.cs @@ -4,6 +4,7 @@ using System.Reactive.Disposables; using System.Reactive.Linq; +using DynamicData.Internal; namespace DynamicData.List.Internal; @@ -14,34 +15,40 @@ internal sealed class MergeManyListChangeSets(IObservable where TObject : notnull where TDestination : notnull { - public IObservable> Run() => - Observable.Create>( - observer => - { - var locker = new object(); - - // This is manages all of the changes - var changeTracker = new ChangeSetMergeTracker(); - - // Transform to a list changeset of child lists, synchronize, and publish. - var shared = source - .Transform(obj => new ClonedListChangeSet(selector(obj).Synchronize(locker), equalityComparer)) - .Synchronize(locker) - .Publish(); - - // Merge the child changeset changes together and apply to the tracker - var subMergeMany = shared - .MergeMany(clonedList => clonedList.Source.RemoveIndex()) - .Subscribe( - changes => changeTracker.ProcessChangeSet(changes, observer), - observer.OnError, - observer.OnCompleted); - - // When a source item is removed, all of its sub-items need to be removed - var subRemove = shared - .OnItemRemoved(clonedList => changeTracker.RemoveItems(clonedList.List, observer), invokeOnUnsubscribe: false) - .Subscribe(); - - return new CompositeDisposable(shared.Connect(), subMergeMany, subRemove); - }); + public IObservable> Run() => Observable.Create>( + observer => + { + var locker = new object(); + var parentUpdate = false; + + // This is manages all of the changes + var changeTracker = new ChangeSetMergeTracker(); + + // Transform to a list changeset of child lists, synchronize, and publish. + var shared = source + .Transform(obj => new ClonedListChangeSet(selector(obj).Synchronize(locker), equalityComparer)) + .Synchronize(locker) + .Do(_ => parentUpdate = true) + .Publish(); + + // Merge the child changeset changes together and apply to the tracker + var subMergeMany = shared + .MergeMany(clonedList => clonedList.Source.RemoveIndex()) + .SubscribeSafe( + changes => changeTracker.ProcessChangeSet(changes, !parentUpdate ? observer : null), + observer.OnError, + observer.OnCompleted); + + // When a source item is removed, all of its sub-items need to be removed + var subRemove = shared + .OnItemRemoved(clonedList => changeTracker.RemoveItems(clonedList.List), invokeOnUnsubscribe: false) + .Do(_ => + { + changeTracker.EmitChanges(observer); + parentUpdate = false; + }) + .Subscribe(); + + return new CompositeDisposable(shared.Connect(), subMergeMany, subRemove); + }); }