Skip to content

FilterImmutable: emit correct Current value when Update transitions to Remove - #1113

Merged
dwcullop merged 3 commits into
reactivemarbles:mainfrom
dwcullop:bugfix/filterimmutable_update_remove
Jun 17, 2026
Merged

dwcullop merged 3 commits into
reactivemarbles:mainfrom
dwcullop:bugfix/filterimmutable_update_remove

Conversation

@dwcullop

Copy link
Copy Markdown
Member

Fixes #1112.

Problem

FilterImmutable emits ChangeReason.Remove carrying the wrong value in Current when an Update changeset entry transitions from matching to not matching the predicate. The emitted Remove carries the new (non-matching) Current instead of the previous (matching) value that actually left downstream.

This violates the Change<T,K> contract that Remove.Current is the item being removed. Consumers reading Current on Remove receive the wrong reference, producing silent corruption (wrong side effects, incorrect projections) or InvalidCastException when composed with type-narrowing operators.

Fix

When emitting Update->Remove, carry change.Previous.Value as Current. Previous is dropped (Remove is not an Update).

Test

Update_PreviousMatchedCurrentDoesNot_EmitsRemoveCarryingPreviousAsCurrent in FilterImmutableFixture covers the contract. RED before the fix, GREEN after.

Verification

  • All 12 tests in FilterImmutableFixture pass.
  • All 1365 tests in DynamicData.Tests.Cache pass.
  • Full suite has 2 pre-existing flakes in DeadlockTortureTest (TransformWithForce_DoesNotDeadlock, AllDangerous_Stacked_DoNotDeadlock) that reproduce on unmodified main. Unrelated to this change.

…rent

When an Update changeset entry has Previous matching the predicate and
Current not matching, FilterImmutable emits a Remove. Previously this
Remove carried the new (non-matching) value as Current, violating the
Change<T,K> contract that Remove.Current is the item being removed (the
item that just left downstream). Since the new value never reached
downstream, only the previous value can satisfy this contract.

Consumers that read Current on Remove (e.g. composition with
TransformImmutable, side-effect handlers like DisposeMany or
OnItemRemoved equivalents) received the wrong reference, silently
producing incorrect results or InvalidCastException.
@dwcullop
dwcullop requested a review from JakenVeina June 14, 2026 22:57
@dwcullop
dwcullop enabled auto-merge (squash) June 14, 2026 22:57
@dwcullop
dwcullop requested a review from Copilot June 14, 2026 22:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a contract violation in the FilterImmutable cache operator where an Update that transitions from included → excluded was emitting a Remove change whose Current value incorrectly contained the new (excluded) item instead of the previous (included) item that actually left the downstream view.

Changes:

  • Adjust FilterImmutable to emit Remove.Current = change.Previous.Value for Update -> Remove transitions.
  • Add a regression test validating the Remove.Current value for this scenario.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/DynamicData/Cache/Internal/FilterImmutable.cs Fixes Update -> Remove emission to carry the previously-included item in Remove.Current.
src/DynamicData.Tests/Cache/FilterImmutableFixture.cs Adds a regression test asserting the corrected Remove.Current contract behavior.

dwcullop and others added 2 commits June 14, 2026 23:49
Per @JakenVeina's review on reactivemarbles#1113: swap .AsAggregator() for
.ValidateChangeSets(Item.KeySelector).RecordCacheItems(out var results)
across all existing FilterImmutable tests. ValidateChangeSets bakes in
the changeset contract rules so each test transitively guards against
shape violations such as a Remove change carrying a Current value that
does not match what was originally added downstream.

Kept the new Update_PreviousMatchedCurrentDoesNot test as a focused
regression that asserts the specific Remove.Current contract.
@dwcullop
dwcullop requested a review from JakenVeina June 15, 2026 07:07
@dwcullop
dwcullop merged commit 6d2144c into reactivemarbles:main Jun 17, 2026
2 checks passed
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Jul 1, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FilterImmutable emits Remove with the wrong Current value for Update->Remove transitions

3 participants

⌂ 换网址