Skip to content

Commit 9f934a2

Browse files
authoredSep 16, 2024··
Fix for ChangeSetMergeTracker so that it correctly works with Value Types (#940)
* Fixes #931 with unit tests * Minor tweaks * Remove an extra equality check
1 parent 741cf6b commit 9f934a2

5 files changed

+64
-26
lines changed
 

‎src/DynamicData.Tests/Cache/MergeChangeSetsFixture.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -651,11 +651,11 @@ public void EqualityComparerAndComparerWorkTogetherForRefreshes()
651651
// then
652652
_marketList.Count.Should().Be(2);
653653
results1.Data.Count.Should().Be(PricesPerMarket);
654-
results1.Messages.Count.Should().Be(2);
654+
results1.Messages.Count.Should().Be(3);
655655
results1.Summary.Overall.Adds.Should().Be(PricesPerMarket);
656656
results1.Summary.Overall.Removes.Should().Be(0);
657657
results1.Summary.Overall.Updates.Should().Be(PricesPerMarket);
658-
results1.Summary.Overall.Refreshes.Should().Be(0);
658+
results1.Summary.Overall.Refreshes.Should().Be(PricesPerMarket);
659659
results2.Messages.Count.Should().Be(4);
660660
results2.Summary.Overall.Adds.Should().Be(PricesPerMarket);
661661
results2.Summary.Overall.Removes.Should().Be(0);

‎src/DynamicData.Tests/Cache/MergeManyChangeSetsCacheFixture.cs

+20
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,26 @@ public void MergedObservableWillFailIfSourceFails()
791791
receivedError.Should().Be(expectedError);
792792
}
793793

794+
[Fact]
795+
public void MergeManyChangeSetsWorksCorrectlyWithValueTypes()
796+
{
797+
// having
798+
var markets = Enumerable.Range(0, MarketCount).Select(n => new Market(n)).ToArray();
799+
_marketCache.AddOrUpdate(markets);
800+
markets.ForEach(m => m.SetPrices(0, PricesPerMarket, GetRandomPrice));
801+
using var results = _marketCache.Connect()
802+
.MergeManyChangeSets(m => m.LatestPrices.Transform(p => p.Price))
803+
.AsAggregator();
804+
805+
// when
806+
markets.ForEach(m => m.RemoveAllPrices());
807+
808+
// then
809+
results.Data.Count.Should().Be(0);
810+
results.Summary.Overall.Adds.Should().Be(PricesPerMarket);
811+
results.Summary.Overall.Removes.Should().Be(PricesPerMarket);
812+
}
813+
794814
public void Dispose()
795815
{
796816
_marketCacheResults.Dispose();

‎src/DynamicData.Tests/Cache/MergeManyChangeSetsCacheSourceCompareFixture.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -921,11 +921,11 @@ public void EqualityComparerAndChildComparerWorkTogetherForRefreshes()
921921
resultsLow.Summary.Overall.Removes.Should().Be(0);
922922
resultsLow.Summary.Overall.Updates.Should().Be(0);
923923
resultsLow.Summary.Overall.Refreshes.Should().Be(0);
924-
resultsRecent.Messages.Count.Should().Be(3);
924+
resultsRecent.Messages.Count.Should().Be(4);
925925
resultsRecent.Summary.Overall.Adds.Should().Be(PricesPerMarket);
926926
resultsRecent.Summary.Overall.Removes.Should().Be(0);
927927
resultsRecent.Summary.Overall.Updates.Should().Be(PricesPerMarket * 2);
928-
resultsRecent.Summary.Overall.Refreshes.Should().Be(0);
928+
resultsRecent.Summary.Overall.Refreshes.Should().Be(PricesPerMarket);
929929
resultsTimeStamp.Messages.Count.Should().Be(5);
930930
resultsTimeStamp.Summary.Overall.Adds.Should().Be(PricesPerMarket);
931931
resultsTimeStamp.Summary.Overall.Removes.Should().Be(0);

‎src/DynamicData/Cache/Internal/ChangeSetMergeTracker.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ internal sealed class ChangeSetMergeTracker<TObject, TKey>(Func<IEnumerable<Chan
1212
where TKey : notnull
1313
{
1414
private readonly ChangeAwareCache<TObject, TKey> _resultCache = new();
15+
private readonly IEqualityComparer<TObject> _equalityComparer = equalityComparer ?? EqualityComparer<TObject>.Default;
1516
private bool _hasCompleted;
1617

1718
public void MarkComplete() => _hasCompleted = true;
@@ -201,7 +202,7 @@ private void OnItemRefreshed(ChangeSetCache<TObject, TKey>[] sources, TObject it
201202
// In the sorting case, a refresh requires doing a full update because any change could alter what the best value is
202203
// If we don't care about sorting OR if we do care, but re-selecting the best value didn't change anything
203204
// AND the current value is the exact one being refreshed, then emit the refresh downstream
204-
if (((comparer is null) || !UpdateToBestValue(sources, key, cached)) && ReferenceEquals(cached.Value, item))
205+
if (((comparer is null) || !UpdateToBestValue(sources, key, cached)) && CheckEquality(cached.Value, item))
205206
{
206207
_resultCache.Refresh(key);
207208
}
@@ -268,9 +269,9 @@ private Optional<TObject> LookupBestValue(ChangeSetCache<TObject, TKey>[] source
268269
}
269270

270271
private bool CheckEquality(TObject left, TObject right) =>
271-
ReferenceEquals(left, right) || (equalityComparer?.Equals(left, right) ?? false);
272+
_equalityComparer.Equals(left, right);
272273

273274
// Return true if candidate should replace current as the observed downstream value
274275
private bool ShouldReplace(TObject candidate, TObject current) =>
275-
!ReferenceEquals(candidate, current) && (comparer?.Compare(candidate, current) < 0);
276+
comparer?.Compare(candidate, current) < 0;
276277
}

‎src/DynamicData/Cache/Internal/ToObservableOptional.cs

+36-19
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,45 @@ internal sealed class ToObservableOptional<TObject, TKey>(IObservable<IChangeSet
1111
where TObject : notnull
1212
where TKey : notnull
1313
{
14+
private readonly IEqualityComparer<TObject> _equalityComparer = equalityComparer ?? EqualityComparer<TObject>.Default;
1415
private readonly IObservable<IChangeSet<TObject, TKey>> _source = source ?? throw new ArgumentNullException(nameof(source));
1516
private readonly TKey _key = key;
1617

1718
public IObservable<Optional<TObject>> Run() => Observable.Create<Optional<TObject>>(observer =>
18-
_source.Subscribe(
19-
changes =>
20-
changes.Where(ShouldEmitChange).ForEach(change => observer.OnNext(change switch
21-
{
22-
{ Reason: ChangeReason.Remove } => Optional.None<TObject>(),
23-
_ => Optional.Some(change.Current),
24-
})),
25-
observer.OnError,
26-
observer.OnCompleted));
27-
28-
private bool ShouldEmitChange(Change<TObject, TKey> change) => change switch
2919
{
30-
{ Key: { } thekey } when !thekey.Equals(_key) => false,
31-
{ Reason: ChangeReason.Add } => true,
32-
{ Reason: ChangeReason.Remove } => true,
33-
{ Reason: ChangeReason.Update, Previous.HasValue: false } => true,
34-
{ Reason: ChangeReason.Update } when equalityComparer is not null => !equalityComparer.Equals(change.Current, change.Previous.Value),
35-
{ Reason: ChangeReason.Update } => !ReferenceEquals(change.Current, change.Previous.Value),
36-
_ => false,
37-
};
20+
var lastValue = Optional.None<TObject>();
21+
22+
return _source.Subscribe(
23+
changes => lastValue = EmitChanges(changes, observer, lastValue),
24+
observer.OnError,
25+
observer.OnCompleted);
26+
});
27+
28+
private Optional<TObject> EmitChanges(IChangeSet<TObject, TKey> changes, IObserver<Optional<TObject>> observer, Optional<TObject> lastValue)
29+
{
30+
foreach (var change in changes.ToConcreteType())
31+
{
32+
// Ignore changes for different keys
33+
if (!change.Key.Equals(_key))
34+
{
35+
continue;
36+
}
37+
38+
// Remove is None, everything else is the current value
39+
var emitValue = change switch
40+
{
41+
{ Reason: ChangeReason.Remove } => Optional.None<TObject>(),
42+
_ => Optional.Some(change.Current),
43+
};
44+
45+
// Emit the value if it has changed
46+
if ((emitValue.HasValue != lastValue.HasValue) || (emitValue.HasValue && !_equalityComparer.Equals(lastValue.Value, emitValue.Value)))
47+
{
48+
observer.OnNext(emitValue);
49+
lastValue = emitValue;
50+
}
51+
}
52+
53+
return lastValue;
54+
}
3855
}

0 commit comments

Comments
 (0)
Please sign in to comment.