Skip to content

[Bug]: Cache dynamic Filter throws InvalidOperationException during ReFilter #1083

Description

@Mankov-A

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

  1. Create a SourceCache<TObject, TKey>.
  2. Connect to it with .Connect().
  3. Apply dynamic cache filtering with .Filter(predicateObservable), where predicateObservable is an IObservable<Func<TObject, bool>>.
  4. Add at least one item to the cache.
  5. Push a new predicate into predicateObservable.
  6. 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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions