From ed51f2a7647e79479e4677acf60374803201001e Mon Sep 17 00:00:00 2001 From: Jake Meiergerd Date: Tue, 1 Sep 2026 00:10:13 -0500 Subject: [PATCH] Fixed bogus lookup logic within list-land `.TransformAsync()`, for processing changes with unspecified indexes, which resulted in `Remove` and `RemoveRange` changes with unspecified indexes to be ignored completely. --- .../List/TransformAsyncFixture.cs | 68 ++++++++++++++++++- .../List/Internal/TransformAsync.cs | 27 ++++++-- 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/src/DynamicData.Tests/List/TransformAsyncFixture.cs b/src/DynamicData.Tests/List/TransformAsyncFixture.cs index be4b8f623..29113c80f 100755 --- a/src/DynamicData.Tests/List/TransformAsyncFixture.cs +++ b/src/DynamicData.Tests/List/TransformAsyncFixture.cs @@ -1,12 +1,14 @@ using System; using System.Linq; -using System.Threading; +using System.Reactive.Linq; using System.Threading.Tasks; -using DynamicData.Kernel; -using DynamicData.Tests.Domain; + using FluentAssertions; using Xunit; +using DynamicData.Tests.Domain; +using DynamicData.Tests.Utilities; + namespace DynamicData.Tests.List; [Obsolete("Not obsolete - test commented out due to test run freezing on Appveyor")] @@ -187,4 +189,64 @@ public void TransformAsyncCancelsTokenOnUnSubscribe() sub.Dispose(); Assert.True(tcs.Task.IsCanceled); } + + // Covers https://github.com/reactivemarbles/DynamicData/issues/1169 + [Fact] + public void RemoveWithoutIndex() + { + var source = Enumerable.Empty>() + .Append(new ChangeSet() + { + new Change( + reason: ListChangeReason.AddRange, + items: Enumerable.Range(1, 3)) + }) + .Append(new ChangeSet() + { + new Change( + reason: ListChangeReason.Remove, + current: 2) + }) + .ToObservable(); + + using var subscription = source + .TransformAsync(static item => Task.FromResult(item.ToString())) + .ValidateChangeSets() + .RecordListItems(out var results); + + results.Error.Should().BeNull("no errors should have occurred"); + results.RecordedChangeSets.Count.Should().Be(2, "2 source operations were performed"); + results.RecordedItems.Should().BeEquivalentTo(new[] { "1", "3" }, static options => options.WithStrictOrdering(), "The middle item in the list should have been removed"); + results.HasCompleted.Should().BeTrue("the source, and all asynchronous operations, have completed"); + } + + // Covers https://github.com/reactivemarbles/DynamicData/issues/1169 + [Fact] + public void RemoveRangeWithoutIndex() + { + var source = Enumerable.Empty>() + .Append(new ChangeSet() + { + new Change( + reason: ListChangeReason.AddRange, + items: Enumerable.Range(1, 5)) + }) + .Append(new ChangeSet() + { + new Change( + reason: ListChangeReason.RemoveRange, + items: new[] { 1, 3, 5 }) + }) + .ToObservable(); + + using var subscription = source + .TransformAsync(static item => Task.FromResult(item.ToString())) + .ValidateChangeSets() + .RecordListItems(out var results); + + results.Error.Should().BeNull("no errors should have occurred"); + results.RecordedChangeSets.Count.Should().Be(2, "2 source operations were performed"); + results.RecordedItems.Should().BeEquivalentTo(new[] { "2", "4" }, static options => options.WithStrictOrdering(), "The odd-numbered items in the list should have been removed"); + results.HasCompleted.Should().BeTrue("the source, and all asynchronous operations, have completed"); + } } diff --git a/src/DynamicData/List/Internal/TransformAsync.cs b/src/DynamicData/List/Internal/TransformAsync.cs index 31f45b1a4..505d86989 100644 --- a/src/DynamicData/List/Internal/TransformAsync.cs +++ b/src/DynamicData/List/Internal/TransformAsync.cs @@ -178,11 +178,16 @@ await _containerFactory( } else { - var toRemove = transformed.FirstOrDefault(t => ReferenceEquals(t.Source, t)); + var indexToRemove = transformed + .Select((pair, index) => ( + sourceItem: pair.Source, + index: (int?)index)) + .FirstOrDefault(pair => EqualityComparer.Default.Equals(pair.sourceItem, change.Current)) + .index; - if (toRemove is not null) + if (indexToRemove is not null) { - transformed.Remove(toRemove); + transformed.RemoveAt(indexToRemove.Value); } } @@ -197,8 +202,20 @@ await _containerFactory( } else { - var toRemove = transformed.Where(t => ReferenceEquals(t.Source, t)).ToArray(); - transformed.RemoveMany(toRemove); + foreach (var removedItem in item.Range) + { + var indexToRemove = transformed + .Select((pair, index) => ( + sourceItem: pair.Source, + index: (int?)index)) + .FirstOrDefault(pair => EqualityComparer.Default.Equals(pair.sourceItem, removedItem)) + .index; + + if (indexToRemove is not null) + { + transformed.RemoveAt(indexToRemove.Value); + } + } } break;