Skip to content

Fixed bogus lookup logic within list-land .TransformAsync(),… - #1171

Open
JakenVeina wants to merge 11 commits into
mainfrom
issues/1169
Open

JakenVeina wants to merge 11 commits into
mainfrom
issues/1169

Conversation

@JakenVeina

Copy link
Copy Markdown
Collaborator

… for processing changes with unspecified indexes, which resulted in Remove and RemoveRange changes with unspecified indexes to be ignored completely.

@JakenVeina JakenVeina linked an issue Sep 1, 2026 that may be closed by this pull request
…ocessing changes with unspecified indexes, which resulted in `Remove` and `RemoveRange` changes with unspecified indexes to be ignored completely.
dwcullop added a commit that referenced this pull request Oct 10, 2026
RemoveWithoutIndex and RemoveRangeWithoutIndex were added by #1171 and dropped while rebasing the global usings onto it.

@dwcullop dwcullop left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fix looks right to me. Matching on EqualityComparer<TSource>.Default lines up with how TransformedItemContainer already defines equality.

The synchronous Transform has the same bug, though, so it'd be good to fix both here:

  • List/Internal/Transformer.cs:146 and :165 match indexless removes with ReferenceEquals(t.Source, change.Current). For value types that's always false because both sides get boxed, so an indexless Remove/RemoveRange of ints is silently dropped, which is exactly #1169.
  • The RemoveRange branch there (Where(t => item.Range.Any(...)) into RemoveMany) also removes every duplicate of an item, not one per entry in the range.
  • TransformFixture.RemoveWithoutIndex only uses Person, so nothing catches it. Mirroring your two new int tests against Transform would.

As it stands sync and async also disagree on what "the same item" means (reference identity vs Equals), which seems worth closing while we're here.

Small nit on the lookup itself: the Select((pair, index) => (sourceItem, (int?)index)).FirstOrDefault(...).index trick takes a second read, allocates per remove, and is duplicated. A plain index loop in one helper that both Transformer and TransformAsync call would be simpler and keeps the two operators from drifting again.

@JakenVeina

Copy link
Copy Markdown
Collaborator Author

Fair point about putting equivalent fixes on .Transform().

I'm not too fussed about the optimalness of the index lookup, when that's SUCH a "this really shouldn't be a thing" scenario, but looking at it, I realized there were actually several OTHER inconsistencies (for other change types) in how index lookups are being done, across both operators. So yeah, I just added a helper method and refactored it into both operators, everywhere an index lookup is needed.

There's still inconsistencies in how the operators handle an index lookup that FAILS (some change types will throw, some will just silently ignore the change) but I opted to just preserve the existing logic, rather than expand scope any further. A failed index lookup really is just an extension of changeset corruption, which is not really this PR's problem to deal with.

…d logic for handling index lookup, when indexes are missing from changesets, into a shared allocation-free helper method.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: TransformAsync on IChangeSet drops events

2 participants