Skip to content

Fixed that .Edit() operations would not defer notifications for new subscriptions occurring during the edit. - #1133

Merged
JakenVeina merged 9 commits into
mainfrom
issues/1129
Sep 17, 2026
Merged

JakenVeina merged 9 commits into
mainfrom
issues/1129

Conversation

@JakenVeina

Copy link
Copy Markdown
Collaborator

Fixed that subscriptions occurring during an .Edit() operation upon a SourceCache<> or SourceList<> would still publish an initial notification to the subscriber. This resulted in the potential for notifications to be duplicated, for items added or manipulated during the edit.

Instead, such subscriptions now have their notifications deferred until the .Edit() is complete.

Resolves #1129.

Comment thread src/DynamicData/Cache/ObservableCache.cs Outdated
Comment thread src/DynamicData/Cache/ObservableCache.cs Outdated
Comment thread src/DynamicData/Cache/ObservableCache.cs Outdated
Comment on lines +129 to +137
: Observable.CombineLatest(
_suspensionTracker.Value.NotificationsSuspendedObservable,
_isEditInProgress,
static (areNotificationsSuspended, isEditInProgress) => areNotificationsSuspended || isEditInProgress)
.Do(static _ => { }, observer.OnCompleted)
.Where(static shouldConnectionBeDeferred => !shouldConnectionBeDeferred)
.Take(1)
.Select(_ => CreateConnectObservable(predicate, suppressEmptyChangeSets))
.Switch();

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.

A subscriber that connects while an edit is in progress never receives OnCompleted when the source is later disposed. It activates fine and receives every changeset, it just never terminates, so anything using Finally for cleanup or awaiting completion waits forever. SourceList behaves the same way.

This isn't yours: the suspension deferral on main already loses completion the same way, and this reuses that shape. The difference is the trigger. Connecting during a suspension is unusual, connecting during an edit is not, so the PR takes a corner case and makes it routine.

Whatever the fix is, it probably belongs on both paths at once, and it might be worth splitting out separately rather than growing this PR.

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.

Also filed #1140. The same deferral converts a source failure into a successful completion, not just a lost completion: a deferred subscriber gets OnCompleted where an ordinary one gets OnError.

Relevant to this PR specifically: routing _isEditInProgress.OnError through the same Do(..., observer.OnCompleted) means an edit-deferred subscriber inherits that behaviour, which widens the trigger from suspensions to ordinary edits.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Let's take that one first then, and I'll make sure it gets properly incorporated here, when I update the branch.

@dwcullop

Copy link
Copy Markdown
Member

Filed this as #1136 so it doesn't get lost if this PR lands first.

Comment thread src/DynamicData/Cache/ObservableCache.cs Outdated
Comment thread src/DynamicData/List/SourceList.cs Outdated
Comment thread src/DynamicData.Tests/Cache/SourceCacheFixture.cs Outdated
Comment thread src/DynamicData/List/SourceList.cs
Comment thread src/DynamicData/Cache/ObservableCache.cs Outdated
Comment thread src/DynamicData/List/SourceList.cs Outdated
Comment thread src/DynamicData/Cache/ObservableCache.cs

@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.

Thanks for working through all of that, it's in much better shape. The lazy seeding with _editLevel is not 0 is a neat touch, it keeps the mid-edit case correct without paying for the subject up front. Nested edits and the failed-edit recovery both have coverage now too.

Two things I'd like to sort out first, both in SourceList. Details inline.

One heads-up as well: this branch predates #1141, which reworked the same deferral in Connect/Watch. Merging main in will conflict there, so worth a careful resolution.

Comment thread src/DynamicData/List/SourceList.cs
Comment thread src/DynamicData/List/SourceList.cs Outdated
Comment thread src/DynamicData/Cache/ObservableCache.cs
Comment thread src/DynamicData.Benchmarks/Miscellaneous/LockImplementations.cs

@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.

Went back over the four from last time against the current head, now that the merge has brought in #1137, #1139 and #1141.

Three of them are gone. Two were mine to withdraw: the list Switch one is fixed by #1139 landing, and the deferral one I simply had wrong, which I have owned in the thread. The benchmark question you had already answered.

The re-entrancy one in SourceList.Edit is still live and I think it is the last thing standing. A subscriber that edits the list from a change handler throws, because InvokeNext now runs while _editLevel is still 1 and the nested edit takes the WriteNested path after the outer write has finished. It works on main, so it is coming from this branch. Suggested shape is in the thread, and the suite is green with it.

Everything else from the first pass looks good. The lazy seeding, the transition guards, SourceList getting the same treatment as the cache, and the extra coverage for nested edits and removals all read well.

@JakenVeina
JakenVeina force-pushed the issues/1129 branch 2 times, most recently from 0f0721d to f130a97 Compare August 6, 2026 07:44
… a `SourceCache<>` or `SourceList<>` would still publish an initial notification to the subscriber. This resulted in the potential for notifications to be duplicated, for items added or manipulated during the edit.

Instead, such subscriptions now have their notifications deferred until the `.Edit()` is complete.

Resolves #1129.
…cking doesn't fall apart if user-injected code throws.
….SuspendNotifications()` does not initialize unless necessary.
…m to the edit-tracking system as well. Also extended the proper strategy for lazy-initialization of both systems from `.Connect()` to `.Watch()`.
…ceList, with some context clarification instead.
…stem in `ObservableCache` to the edit-tracking system in `SourceList`.
@JakenVeina
JakenVeina merged commit 85ee49c into main Sep 17, 2026
2 checks passed
@JakenVeina
JakenVeina deleted the issues/1129 branch September 17, 2026 23:55
JakenVeina added a commit that referenced this pull request Sep 21, 2026
… subscriptions occurring during the edit. (#1133)

* Fixed that subscriptions occurring during an `.Edit()` operation upon a `SourceCache<>` or `SourceList<>` would still publish an initial notification to the subscriber. This resulted in the potential for notifications to be duplicated, for items added or manipulated during the edit.

Instead, such subscriptions now have their notifications deferred until the `.Edit()` is complete.

Resolves #1129.

* Added exception guarantees to edit level tracking, to ensure that tracking doesn't fall apart if user-injected code throws.

* Adjusted `.Connect()` to ensure that the internal system to support `.SuspendNotifications()` does not initialize unless necessary.

* Extended the strategy of lazy-initialization for the suspension system to the edit-tracking system as well. Also extended the proper strategy for lazy-initialization of both systems from `.Connect()` to `.Watch()`.

* Added logic to avoid redundant `_isEditInProgress` notifications.

* Removed redundant completion signal on _isEditInProgress, within SourceList, with some context clarification instead.

* Adjusted the new test for covering #1129, to also exercise removals and nested edits.

* Extended the strategy of lazy-initialization for the edit-tracking system in `ObservableCache` to the edit-tracking system in `SourceList`.

---------

Co-authored-by: Darrin W. Cullop <Darrin.Cullop@microsoft.com>
@JakenVeina JakenVeina mentioned this pull request Sep 21, 2026
JakenVeina added a commit that referenced this pull request Sep 21, 2026
… subscriptions occurring during the edit. (#1133)

* Fixed that subscriptions occurring during an `.Edit()` operation upon a `SourceCache<>` or `SourceList<>` would still publish an initial notification to the subscriber. This resulted in the potential for notifications to be duplicated, for items added or manipulated during the edit.

Instead, such subscriptions now have their notifications deferred until the `.Edit()` is complete.

Resolves #1129.

* Added exception guarantees to edit level tracking, to ensure that tracking doesn't fall apart if user-injected code throws.

* Adjusted `.Connect()` to ensure that the internal system to support `.SuspendNotifications()` does not initialize unless necessary.

* Extended the strategy of lazy-initialization for the suspension system to the edit-tracking system as well. Also extended the proper strategy for lazy-initialization of both systems from `.Connect()` to `.Watch()`.

* Added logic to avoid redundant `_isEditInProgress` notifications.

* Removed redundant completion signal on _isEditInProgress, within SourceList, with some context clarification instead.

* Adjusted the new test for covering #1129, to also exercise removals and nested edits.

* Extended the strategy of lazy-initialization for the edit-tracking system in `ObservableCache` to the edit-tracking system in `SourceList`.

---------

Co-authored-by: Darrin W. Cullop <Darrin.Cullop@microsoft.com>
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]: ArgumentException: An item with the same key was already added with Filter on 9.4.31+

2 participants