Skip to content

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

@Mankov-A

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.

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