Describe the bug 🐞
After upgrading DynamicData to 9.4.31, changing a dynamic cache filter predicate started throwing InvalidOperationException.
The same code worked with DynamicData 9.1.2.
The operator shape is:
sourceCache
.Connect()
.Filter(predicateObservable)
where predicateObservable is an IObservable<Func<TObject, bool>>.
Exception:
System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
at System.Collections.Generic.Dictionary`2.KeyCollection.Enumerator.MoveNext()
at DynamicData.Cache.Internal.Filter.Dynamic`3.Subscription.ReFilter(TState predicateState)
at DynamicData.Cache.Internal.Filter.Dynamic`3.Subscription.OnPredicateStateNext(TState predicateState)
I suspect the issue is in Cache/Internal/Filter.Dynamic.cs, in ReFilter.
The code appears to enumerate dictionary keys and update the same dictionary inside the loop:
foreach (var key in _itemStatesByKey.Keys)
{
var itemState = _itemStatesByKey[key];
var isIncluded = _predicate.Invoke(predicateState, itemState.Item);
_itemStatesByKey[key] = new()
{
IsIncluded = isIncluded,
Item = itemState.Item
};
}
On .NET Framework, assigning an existing Dictionary<TKey, TValue> value invalidates enumeration of Dictionary.Keys, so MoveNext() throws InvalidOperationException.
I have not verified whether the same repro fails on .NET 6/8/9.
Step to reproduce
- Create a
SourceCache<TObject, TKey>.
- Connect to it with
.Connect().
- Apply dynamic cache filtering with
.Filter(predicateObservable), where predicateObservable is an IObservable<Func<TObject, bool>>.
- Add at least one item to the cache.
- Push a new predicate into
predicateObservable.
- Observe
InvalidOperationException from Filter.Dynamic.Subscription.ReFilter.
Reproduction repository
Expected behavior
Changing the dynamic predicate should re-filter the cache without throwing.
Screenshots 🖼️
No response
IDE
No response
Operating system
No response
Version
No response
Device
No response
DynamicData Version
9.4.31
Additional information ℹ️
Real stack trace from the app:
System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
at System.ThrowHelper.ThrowInvalidOperationException(ExceptionResource resource)
at System.Collections.Generic.Dictionary`2.KeyCollection.Enumerator.MoveNext()
at DynamicData.Cache.Internal.Filter.Dynamic`3.Subscription.ReFilter(TState predicateState)
at DynamicData.Cache.Internal.Filter.Dynamic`3.Subscription.OnPredicateStateNext(TState predicateState)
at System.Reactive.AnonymousObserver`1.OnNextCore(T value)
at System.Reactive.ObserverBase`1.OnNext(T value)
at System.Reactive.Sink`1.ForwardOnNext(TTarget value)
at System.Reactive.Linq.ObservableImpl.Select`2.Selector._.OnNext(TSource value)
at System.Reactive.Sink`1.ForwardOnNext(TTarget value)
at System.Reactive.Linq.ObservableImpl.Throttle`1._.Propagate()
Possible fix would be to avoid mutating the dictionary while enumerating Keys, for example by iterating over a snapshot of keys or by buffering state updates and applying them after enumeration.
Describe the bug 🐞
After upgrading DynamicData to 9.4.31, changing a dynamic cache filter predicate started throwing
InvalidOperationException.The same code worked with DynamicData 9.1.2.
The operator shape is:
where
predicateObservableis anIObservable<Func<TObject, bool>>.Exception:
I suspect the issue is in
Cache/Internal/Filter.Dynamic.cs, inReFilter.The code appears to enumerate dictionary keys and update the same dictionary inside the loop:
On .NET Framework, assigning an existing
Dictionary<TKey, TValue>value invalidates enumeration ofDictionary.Keys, soMoveNext()throwsInvalidOperationException.I have not verified whether the same repro fails on .NET 6/8/9.
Step to reproduce
SourceCache<TObject, TKey>..Connect()..Filter(predicateObservable), wherepredicateObservableis anIObservable<Func<TObject, bool>>.predicateObservable.InvalidOperationExceptionfromFilter.Dynamic.Subscription.ReFilter.Reproduction repository
Expected behavior
Changing the dynamic predicate should re-filter the cache without throwing.
Screenshots 🖼️
No response
IDE
No response
Operating system
No response
Version
No response
Device
No response
DynamicData Version
9.4.31
Additional information ℹ️
Real stack trace from the app:
Possible fix would be to avoid mutating the dictionary while enumerating
Keys, for example by iterating over a snapshot of keys or by buffering state updates and applying them after enumeration.