diff --git a/src/DynamicDataVNext/Distinct/ChangeTrackingSet.cs b/src/DynamicDataVNext/Distinct/ChangeTrackingSet.cs
new file mode 100644
index 0000000..cbd9449
--- /dev/null
+++ b/src/DynamicDataVNext/Distinct/ChangeTrackingSet.cs
@@ -0,0 +1,495 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace DynamicDataVNext;
+
+///
+/// A collection of distinct items, which tracks and allows for capturing of changes made to it, as they occur.
+///
+/// The type of the items in the collection.
+public sealed class ChangeTrackingSet
+ : IExtendedSet,
+ IReadOnlySet
+{
+ private readonly DistinctChangeSet.Builder _changeCollector;
+ private readonly IEqualityComparer _comparer;
+
+ private bool _isChangeCollectionEnabled;
+ private bool _isDirty;
+ private HashSet _items;
+
+ ///
+ /// Constructs a new instance of the class.
+ ///
+ /// The initial number of items that the collection should be able to contain, before needing to allocation additional memory.
+ /// The comparer to be used for detecting item changes, within the collection, or if should be used.
+ public ChangeTrackingSet(
+ int? capacity = null,
+ IEqualityComparer? comparer = null)
+ {
+ _comparer = comparer ?? EqualityComparer.Default;
+
+ _items = (capacity is int givenCapacity)
+ ? new(
+ capacity: givenCapacity,
+ comparer: _comparer)
+ : new(
+ comparer: _comparer);
+
+ _changeCollector = new();
+ _isChangeCollectionEnabled = true;
+ }
+
+ ///
+ /// The comparer to be used for matching items against each other.
+ ///
+ public IEqualityComparer Comparer
+ => _items.Comparer;
+
+ ///
+ public int Count
+ => _items.Count;
+
+ ///
+ /// A flag indicating whether the collection should actually collect changes, to be retrieved by calls to .
+ /// Defaults to .
+ ///
+ ///
+ /// Note that any changes previously collected, but not captured, when this property is set to will be discarded. Otherwise, it would be possible for to generate a corrupt changes, when next called, after setting this property back to .
+ ///
+ public bool IsChangeCollectionEnabled
+ {
+ get => _isChangeCollectionEnabled;
+ set
+ {
+ if ((value is false) && (_changeCollector.Count is not 0))
+ _changeCollector.Clear();
+
+ _isChangeCollectionEnabled = value;
+ }
+ }
+
+ ///
+ /// A flag indicating whether changes have been made to the collection since its creation, or since the last call to `.
+ ///
+ public bool IsDirty
+ => _isDirty;
+
+ ///
+ /// Captures any previously-collected changes made to the collection, and resets the collection to a "clean" state (I.E. sets to ).
+ ///
+ /// A containing all changes made to the collection since its construction, the last call to , or since was last changed to .
+ ///
+ /// Note that this method will always return an empty changeset, when is .
+ ///
+ public DistinctChangeSet CaptureChangesAndClean()
+ {
+ _isDirty = false;
+ return _changeCollector.BuildAndClear();
+ }
+
+ ///
+ public bool Add(T item)
+ {
+ var result = _items.Add(item);
+
+ if (result && _isChangeCollectionEnabled)
+ _changeCollector.AddChange(DistinctChange.Addition(item));
+
+ _isDirty |= result;
+
+ return result;
+ }
+
+ ///
+ public void Clear()
+ {
+ if (_items.Count is 0)
+ return;
+
+ if (_isChangeCollectionEnabled)
+ {
+ foreach (var item in _items)
+ _changeCollector.AddChange(DistinctChange.Removal(item));
+
+ _changeCollector.OnSourceCleared();
+ }
+
+ _items.Clear();
+
+ _isDirty = true;
+ }
+
+ ///
+ public bool Contains(T item)
+ => _items.Contains(item);
+
+ ///
+ public void CopyTo(
+ T[] array,
+ int arrayIndex)
+ => _items.CopyTo(array, arrayIndex);
+
+ ///
+ public void EnsureCapacity(int capacity)
+ => _items.EnsureCapacity(capacity);
+
+ ///
+ public void ExceptWith(IEnumerable other)
+ {
+ if (_items.Count is 0)
+ return;
+
+ if (_isChangeCollectionEnabled)
+ {
+ if(other.TryGetNonEnumeratedCount(out var otherCount))
+ {
+ if (otherCount is 0)
+ return;
+
+ _changeCollector.EnsureCapacity(Math.Min(otherCount, _items.Count));
+ }
+
+ foreach (var item in other)
+ if (_items.Remove(item))
+ {
+ _changeCollector.AddChange(DistinctChange.Removal(item));
+ _isDirty = true;
+ }
+
+ if (_items.Count is 0)
+ _changeCollector.OnSourceCleared();
+ }
+ else
+ {
+ var oldCount = _items.Count;
+
+ _items.ExceptWith(other);
+
+ _isDirty |= _items.Count != oldCount;
+ }
+ }
+
+ ///
+ public void ExceptWith(ReadOnlySpan other)
+ {
+ if ((other.Length is 0) || (_items.Count is 0))
+ return;
+
+ if (_isChangeCollectionEnabled)
+ {
+ _changeCollector.EnsureCapacity(Math.Min(other.Length, _items.Count));
+
+ foreach (var item in other)
+ if (_items.Remove(item))
+ {
+ _changeCollector.AddChange(DistinctChange.Removal(item));
+ _isDirty = true;
+ }
+
+ if (_items.Count is 0)
+ _changeCollector.OnSourceCleared();
+ }
+ else
+ {
+ foreach (var item in other)
+ _isDirty |= _items.Remove(item);
+ }
+ }
+
+ ///
+ public void IntersectWith(IEnumerable other)
+ {
+ if (_items.Count is 0)
+ return;
+
+ if (_isChangeCollectionEnabled)
+ {
+ if(other.TryGetNonEnumeratedCount(out var otherCount))
+ _changeCollector.EnsureCapacity(Math.Min(0, _items.Count - otherCount));
+
+ var newItems = new HashSet(capacity: Math.Min(otherCount, _items.Count));
+ var hasChanges = false;
+ foreach (var item in other)
+ {
+ if (_items.Remove(item))
+ newItems.Add(item);
+ else
+ {
+ _changeCollector.AddChange(DistinctChange.Removal(item));
+ hasChanges = true;
+ }
+ }
+
+ if (hasChanges)
+ {
+ _items = newItems;
+ _isDirty = true;
+
+ if (_items.Count is 0)
+ _changeCollector.OnSourceCleared();
+ }
+ }
+ else
+ {
+ var oldCount = _items.Count;
+
+ _items.IntersectWith(other);
+
+ _isDirty = _items.Count != oldCount;
+ }
+ }
+
+ ///
+ public void IntersectWith(ReadOnlySpan other)
+ {
+ if (_items.Count is 0)
+ return;
+
+ var newItems = new HashSet(capacity: Math.Min(other.Length, _items.Count));
+ var hasChanges = false;
+
+ if (_isChangeCollectionEnabled)
+ {
+ _changeCollector.EnsureCapacity(Math.Min(0, _items.Count - other.Length));
+
+ foreach (var item in other)
+ {
+ if (_items.Remove(item))
+ newItems.Add(item);
+ else
+ {
+ _changeCollector.AddChange(DistinctChange.Removal(item));
+ hasChanges = true;
+ }
+ }
+ }
+ else
+ {
+ foreach (var item in other)
+ {
+ if (_items.Remove(item))
+ newItems.Add(item);
+ else
+ hasChanges = true;
+ }
+ }
+
+ if (hasChanges)
+ {
+ _items = newItems;
+ _isDirty = true;
+
+ if (_isChangeCollectionEnabled && (_items.Count is 0))
+ _changeCollector.OnSourceCleared();
+ }
+ }
+
+ ///
+ public HashSet.Enumerator GetEnumerator()
+ => _items.GetEnumerator();
+
+ ///
+ public bool IsProperSubsetOf(IEnumerable other)
+ => _items.IsProperSubsetOf(other);
+
+ ///
+ public bool IsProperSupersetOf(IEnumerable other)
+ => _items.IsProperSupersetOf(other);
+
+ ///
+ public bool IsSubsetOf(IEnumerable other)
+ => _items.IsSubsetOf(other);
+
+ ///
+ public bool IsSupersetOf(IEnumerable other)
+ => _items.IsSupersetOf(other);
+
+ ///
+ public bool Overlaps(IEnumerable other)
+ => _items.Overlaps(other);
+
+ ///
+ public bool Remove(T item)
+ {
+ var result = _items.Remove(item);
+
+ if (result && _isChangeCollectionEnabled)
+ _changeCollector.AddChange(DistinctChange.Removal(item));
+
+ _isDirty |= result;
+
+ return result;
+ }
+
+ ///
+ public void Reset(IEnumerable items)
+ {
+ Clear();
+ UnionWith(items);
+ }
+
+ ///
+ public void Reset(ReadOnlySpan items)
+ {
+ Clear();
+ UnionWith(items);
+ }
+
+ ///
+ public bool SetEquals(IEnumerable other)
+ => _items.SetEquals(other);
+
+ ///
+ public void SymmetricExceptWith(IEnumerable other)
+ {
+ if (_isChangeCollectionEnabled)
+ {
+ if(other.TryGetNonEnumeratedCount(out var otherCount))
+ {
+ if (otherCount is 0)
+ return;
+
+ _items.EnsureCapacity(_items.Count + otherCount);
+ _changeCollector.EnsureCapacity(_items.Count + otherCount);
+ }
+
+ foreach (var item in other)
+ {
+ if (_items.Add(item))
+ _changeCollector.AddChange(DistinctChange.Addition(item));
+ else
+ {
+ _items.Remove(item);
+ _changeCollector.AddChange(DistinctChange.Removal(item));
+ }
+
+ _isDirty = true;
+ }
+
+ if (_items.Count is 0)
+ _changeCollector.OnSourceCleared();
+ }
+ else
+ {
+ foreach (var item in other)
+ {
+ if (!_items.Add(item))
+ _items.Remove(item);
+
+ _isDirty = true;
+ }
+ }
+ }
+
+ ///
+ public void SymmetricExceptWith(ReadOnlySpan other)
+ {
+ if (other.Length is 0)
+ return;
+
+ if (_isChangeCollectionEnabled)
+ {
+ _items.EnsureCapacity(_items.Count + other.Length);
+ _changeCollector.EnsureCapacity(_items.Count + other.Length);
+
+ foreach (var item in other)
+ {
+ if (_items.Add(item))
+ _changeCollector.AddChange(DistinctChange.Addition(item));
+ else
+ {
+ _items.Remove(item);
+ _changeCollector.AddChange(DistinctChange.Removal(item));
+ }
+
+ _isDirty = true;
+ }
+
+ if (_items.Count is 0)
+ _changeCollector.OnSourceCleared();
+ }
+ else
+ {
+ foreach (var item in other)
+ {
+ if (!_items.Add(item))
+ _items.Remove(item);
+
+ _isDirty = true;
+ }
+ }
+ }
+
+ ///
+ public void UnionWith(IEnumerable other)
+ {
+ if (_isChangeCollectionEnabled)
+ {
+ if (other.TryGetNonEnumeratedCount(out var otherCount))
+ {
+ if (otherCount is 0)
+ return;
+
+ _items.EnsureCapacity(_items.Count + otherCount);
+
+ _changeCollector.EnsureCapacity(otherCount);
+ }
+
+ foreach(var item in other)
+ if (_items.Add(item))
+ {
+ _changeCollector.AddChange(DistinctChange.Addition(item));
+ _isDirty = true;
+ }
+ }
+ else
+ {
+ var oldCount = _items.Count;
+
+ _items.UnionWith(other);
+
+ _isDirty |= oldCount != _items.Count;
+ }
+ }
+
+ ///
+ public void UnionWith(ReadOnlySpan other)
+ {
+ if (other.Length is 0)
+ return;
+
+ _items.EnsureCapacity(_items.Count + other.Length);
+
+ if (_isChangeCollectionEnabled)
+ {
+ _changeCollector.EnsureCapacity(other.Length);
+
+ foreach(var item in other)
+ if (_items.Add(item))
+ {
+ _changeCollector.AddChange(DistinctChange.Addition(item));
+ _isDirty = true;
+ }
+ }
+ else
+ {
+ foreach(var item in other)
+ _isDirty |= _items.Add(item);
+ }
+ }
+
+ bool ICollection.IsReadOnly
+ => false;
+
+ void ICollection.Add(T item)
+ => Add(item);
+
+ IEnumerator IEnumerable.GetEnumerator()
+ => _items.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator()
+ => _items.GetEnumerator();
+}
diff --git a/src/DynamicDataVNext/Distinct/DistinctChangeSet.cs b/src/DynamicDataVNext/Distinct/DistinctChangeSet.cs
index 2350e31..4b4d763 100644
--- a/src/DynamicDataVNext/Distinct/DistinctChangeSet.cs
+++ b/src/DynamicDataVNext/Distinct/DistinctChangeSet.cs
@@ -30,7 +30,7 @@ public static DistinctChangeSet Addition(T item)
/// The type of the items being added.
/// The items being added.
/// A describing the addition of the given items.
- public static DistinctChangeSet Addition(IEnumerable items)
+ public static DistinctChangeSet BulkAddition(IEnumerable items)
{
if (!items.TryGetNonEnumeratedCount(out var itemsCount))
itemsCount = 0;
@@ -47,8 +47,8 @@ public static DistinctChangeSet Addition(IEnumerable items)
};
}
- ///
- public static DistinctChangeSet Addition(ReadOnlySpan items)
+ ///
+ public static DistinctChangeSet BulkAddition(ReadOnlySpan items)
{
var changes = ImmutableArray.CreateBuilder>(initialCapacity: items.Length);
@@ -63,12 +63,12 @@ public static DistinctChangeSet Addition(ReadOnlySpan items)
}
///
- /// Creates a new representing the clearing of a collection of distinct items.
+ /// Creates a new representing the removal of a range of items.
///
/// The type of the items being removed.
/// The items being removed.
- /// A describing the clearing of the collection.
- public static DistinctChangeSet Clear(IEnumerable items)
+ /// A describing the removal of the given items.
+ public static DistinctChangeSet BulkRemoval(IEnumerable items)
{
if (!items.TryGetNonEnumeratedCount(out var itemsCount))
itemsCount = 0;
@@ -81,12 +81,12 @@ public static DistinctChangeSet Clear(IEnumerable items)
return new()
{
Changes = changes.MoveToOrCreateImmutable(),
- Type = ChangeSetType.Clear
+ Type = ChangeSetType.Update
};
}
- ///
- public static DistinctChangeSet Clear(ReadOnlySpan items)
+ ///
+ public static DistinctChangeSet BulkRemoval(ReadOnlySpan items)
{
var changes = ImmutableArray.CreateBuilder>(initialCapacity: items.Length);
@@ -96,30 +96,17 @@ public static DistinctChangeSet Clear(ReadOnlySpan items)
return new()
{
Changes = changes.MoveToImmutable(),
- Type = ChangeSetType.Clear
- };
- }
-
- ///
- /// Creates a new representing the removal of a single item.
- ///
- /// The type of the item being removed.
- /// The item being removed.
- /// A describing the removal of the given item.
- public static DistinctChangeSet Removal(T item)
- => new()
- {
- Changes = ImmutableArray.Create(DistinctChange.Removal(item)),
Type = ChangeSetType.Update
};
+ }
///
- /// Creates a new representing the removal of a range of items.
+ /// Creates a new representing the clearing of a collection of distinct items.
///
/// The type of the items being removed.
/// The items being removed.
- /// A describing the removal of the given items.
- public static DistinctChangeSet Removal(IEnumerable items)
+ /// A describing the clearing of the collection.
+ public static DistinctChangeSet Clear(IEnumerable items)
{
if (!items.TryGetNonEnumeratedCount(out var itemsCount))
itemsCount = 0;
@@ -132,12 +119,12 @@ public static DistinctChangeSet Removal(IEnumerable items)
return new()
{
Changes = changes.MoveToOrCreateImmutable(),
- Type = ChangeSetType.Update
+ Type = ChangeSetType.Clear
};
}
- ///
- public static DistinctChangeSet Removal(ReadOnlySpan items)
+ ///
+ public static DistinctChangeSet Clear(ReadOnlySpan items)
{
var changes = ImmutableArray.CreateBuilder>(initialCapacity: items.Length);
@@ -147,10 +134,23 @@ public static DistinctChangeSet Removal(ReadOnlySpan items)
return new()
{
Changes = changes.MoveToImmutable(),
- Type = ChangeSetType.Update
+ Type = ChangeSetType.Clear
};
}
+ ///
+ /// Creates a new representing the removal of a single item.
+ ///
+ /// The type of the item being removed.
+ /// The item being removed.
+ /// A describing the removal of the given item.
+ public static DistinctChangeSet Removal(T item)
+ => new()
+ {
+ Changes = ImmutableArray.Create(DistinctChange.Removal(item)),
+ Type = ChangeSetType.Update
+ };
+
///
/// Creates a new representing the resetting of items in a collection of distinct items.
///
diff --git a/src/DynamicDataVNext/Distinct/IExtendedSet.cs b/src/DynamicDataVNext/Distinct/IExtendedSet.cs
new file mode 100644
index 0000000..1a7a091
--- /dev/null
+++ b/src/DynamicDataVNext/Distinct/IExtendedSet.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+
+namespace DynamicDataVNext;
+
+///
+/// Describes an extended version of , supporting range operations.
+///
+/// The type of the items in the collection.
+public interface IExtendedSet
+ : ISet
+{
+ /// .
+ void ExceptWith(ReadOnlySpan other);
+
+ /// .
+ void IntersectWith(ReadOnlySpan other);
+
+ ///
+ /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation.
+ ///
+ /// The set of items with which to populate the collection.
+ /// Throws for .
+ void Reset(IEnumerable items);
+
+ ///
+ /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation.
+ ///
+ /// The set of items with which to populate the collection.
+ void Reset(ReadOnlySpan items);
+
+ /// .
+ void SymmetricExceptWith(ReadOnlySpan other);
+
+ /// .
+ void UnionWith(ReadOnlySpan other);
+}
diff --git a/src/DynamicDataVNext/Distinct/ISubjectSet.cs b/src/DynamicDataVNext/Distinct/ISubjectSet.cs
index a6eeab9..5307c72 100644
--- a/src/DynamicDataVNext/Distinct/ISubjectSet.cs
+++ b/src/DynamicDataVNext/Distinct/ISubjectSet.cs
@@ -9,7 +9,7 @@ namespace DynamicDataVNext;
///
/// The type of the items in the collection.
public interface ISubjectSet
- : ISet,
+ : IExtendedSet,
IObservable>
{
///
@@ -17,13 +17,6 @@ public interface ISubjectSet
///
IObservable CollectionChanged { get; }
- ///
- /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation.
- ///
- /// The set of items with which to populate the collection.
- /// Throws for .
- void Reset(IEnumerable items);
-
///
/// Temporarily suspends the publication of notifications by the collection, until the returned object is disposed, at which point all mutations made during the suspension will (if any) will be published as one notification.
///
diff --git a/src/DynamicDataVNext/Distinct/SubjectSet.cs b/src/DynamicDataVNext/Distinct/SubjectSet.cs
new file mode 100644
index 0000000..60a532c
--- /dev/null
+++ b/src/DynamicDataVNext/Distinct/SubjectSet.cs
@@ -0,0 +1,299 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reactive;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+
+namespace DynamicDataVNext;
+
+///
+/// The basic implementation of , providing simple collection and change notification functionality, with no concurrency or thread-safety.
+///
+/// The type of the items in the collection.
+public sealed class SubjectSet
+ : ISubjectSet,
+ IObservableSet,
+ IDisposable
+ where T : notnull
+{
+ private readonly Subject> _changeStream;
+ private readonly Subject _collectionChanged;
+ private readonly Subject _notificationsResumed;
+ private readonly Action _onChangeStreamFinalized;
+ private readonly ChangeTrackingSet _items;
+
+ private int _notificationSuspensionCount;
+
+ ///
+ /// Constructs a new instance of the class.
+ ///
+ /// The initial number of items that the collection should be able to contain, before needing to allocation additional memory.
+ /// The comparer to be used for detecting item changes, within the collection, or if should be used.
+ public SubjectSet(
+ int? capacity = null,
+ IEqualityComparer? comparer = null)
+ {
+ _collectionChanged = new();
+ _changeStream = new();
+ _notificationsResumed = new();
+ _items = new(
+ capacity: capacity,
+ comparer: comparer);
+
+ _onChangeStreamFinalized = () => _items.IsChangeCollectionEnabled = _changeStream.HasObservers;
+ }
+
+ ///
+ public IObservable CollectionChanged
+ => _collectionChanged;
+
+ ///
+ public IEqualityComparer Comparer
+ => _items.Comparer;
+
+ ///
+ public int Count
+ => _items.Count;
+
+ ///
+ public bool Add(T item)
+ {
+ var result = _items.Add(item);
+
+ PublishPendingNotifications();
+
+ return result;
+ }
+
+ ///
+ public void Clear()
+ {
+ _items.Clear();
+
+ PublishPendingNotifications();
+ }
+
+ ///
+ public bool Contains(T item)
+ => _items.Contains(item);
+
+ ///
+ public void CopyTo(T[] array, int arrayIndex)
+ => _items.CopyTo(array, arrayIndex);
+
+ ///
+ public void Dispose()
+ {
+ _changeStream .OnCompleted();
+ _collectionChanged .OnCompleted();
+ _notificationsResumed .OnCompleted();
+
+ _changeStream .Dispose();
+ _collectionChanged .Dispose();
+ _notificationsResumed .Dispose();
+ }
+
+ ///
+ public void EnsureCapacity(int capacity)
+ => _items.EnsureCapacity(capacity);
+
+ ///
+ public void ExceptWith(ReadOnlySpan other)
+ {
+ _items.ExceptWith(other);
+
+ PublishPendingNotifications();
+ }
+
+ ///
+ public void ExceptWith(IEnumerable other)
+ {
+ _items.ExceptWith(other);
+
+ PublishPendingNotifications();
+ }
+
+ ///
+ public HashSet.Enumerator GetEnumerator()
+ => _items.GetEnumerator();
+
+ ///
+ public void IntersectWith(ReadOnlySpan other)
+ {
+ _items.IntersectWith(other);
+
+ PublishPendingNotifications();
+ }
+
+ ///
+ public void IntersectWith(IEnumerable other)
+ {
+ _items.IntersectWith(other);
+
+ PublishPendingNotifications();
+ }
+
+ ///
+ public bool IsProperSubsetOf(IEnumerable other)
+ => _items.IsProperSubsetOf(other);
+
+ ///
+ public bool IsProperSupersetOf(IEnumerable other)
+ => _items.IsProperSupersetOf(other);
+
+ ///
+ public bool IsSubsetOf(IEnumerable other)
+ => _items.IsSubsetOf(other);
+
+ ///
+ public bool IsSupersetOf(IEnumerable other)
+ => _items.IsSupersetOf(other);
+
+ ///
+ public bool Overlaps(IEnumerable other)
+ => _items.Overlaps(other);
+
+ ///
+ public bool Remove(T item)
+ {
+ var result = _items.Remove(item);
+
+ PublishPendingNotifications();
+
+ return result;
+ }
+
+ ///
+ public void Reset(IEnumerable items)
+ {
+ _items.Reset(items);
+
+ PublishPendingNotifications();
+ }
+
+ ///
+ public void Reset(ReadOnlySpan items)
+ {
+ _items.Reset(items);
+
+ PublishPendingNotifications();
+ }
+ ///
+ public bool SetEquals(IEnumerable other)
+ => _items.SetEquals(other);
+
+ ///
+ public IDisposable Subscribe(IObserver> observer)
+ {
+ _items.IsChangeCollectionEnabled = true;
+
+ return ((_notificationSuspensionCount is 0)
+ ? Observable.Empty()
+ : _notificationsResumed
+ .Take(1))
+ .Select(_ => _changeStream
+ .Finally(_onChangeStreamFinalized)
+ .Prepend(DistinctChangeSet.BulkAddition(_items)))
+ .Switch()
+ .Subscribe(observer);
+ }
+
+ ///
+ public NotificationSuspension SuspendNotifications()
+ {
+ ++_notificationSuspensionCount;
+ return new(this);
+ }
+
+ ///
+ public void SymmetricExceptWith(ReadOnlySpan other)
+ {
+ _items.SymmetricExceptWith(other);
+
+ PublishPendingNotifications();
+ }
+
+ ///
+ public void SymmetricExceptWith(IEnumerable other)
+ {
+ _items.SymmetricExceptWith(other);
+
+ PublishPendingNotifications();
+ }
+
+ ///
+ public void UnionWith(ReadOnlySpan other)
+ {
+ _items.UnionWith(other);
+
+ PublishPendingNotifications();
+ }
+
+ ///
+ public void UnionWith(IEnumerable other)
+ {
+ _items.UnionWith(other);
+
+ PublishPendingNotifications();
+ }
+
+ bool ICollection.IsReadOnly
+ => false;
+
+ void ICollection.Add(T item)
+ => Add(item);
+
+ IEnumerator IEnumerable.GetEnumerator()
+ => _items.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator()
+ => _items.GetEnumerator();
+
+ IDisposable ISubjectSet.SuspendNotifications()
+ => SuspendNotifications();
+
+ private void OnNotificationSuspensionDisposed()
+ {
+ --_notificationSuspensionCount;
+ if (_notificationSuspensionCount is 0)
+ {
+ PublishPendingNotifications();
+
+ _notificationsResumed.OnNext(Unit.Default);
+ }
+ }
+
+ private void PublishPendingNotifications()
+ {
+ if ((_notificationSuspensionCount is not 0) || !_items.IsDirty)
+ return;
+
+ _collectionChanged.OnNext(Unit.Default);
+
+ _changeStream.OnNext(_items.CaptureChangesAndClean());
+ }
+
+ ///
+ /// A value that controls suspension of notifications, for a . Will trigger notifications to be resumed, when disposed.
+ ///
+ public struct NotificationSuspension
+ : IDisposable
+ {
+ private SubjectSet? _owner;
+
+ internal NotificationSuspension(SubjectSet owner)
+ => _owner = owner;
+
+ ///
+ public void Dispose()
+ {
+ if (_owner is not null)
+ {
+ _owner.OnNotificationSuspensionDisposed();
+ _owner = null;
+ }
+ }
+ }
+}
diff --git a/src/DynamicDataVNext/Keyed/ChangeTrackingCache.cs b/src/DynamicDataVNext/Keyed/ChangeTrackingCache.cs
new file mode 100644
index 0000000..a6facd1
--- /dev/null
+++ b/src/DynamicDataVNext/Keyed/ChangeTrackingCache.cs
@@ -0,0 +1,367 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Net.Http.Headers;
+
+namespace DynamicDataVNext;
+
+///
+/// A collection of distinctly-keyed items, which tracks and allows for capturing of changes made to it, as they occur.
+///
+/// The type of the key values of items in the collection.
+/// The type of the items in the collection.
+public sealed class ChangeTrackingCache
+ : ICache,
+ IReadOnlyCache
+ where TKey : notnull
+{
+ private readonly KeyedChangeSet.Builder _changeCollector;
+ private readonly IEqualityComparer _itemComparer;
+ private readonly Dictionary _itemsByKey;
+ private readonly Func _keySelector;
+
+ private bool _isChangeCollectionEnabled;
+ private bool _isDirty;
+
+ ///
+ /// Constructs a new instance of the class.
+ ///
+ /// The initial number of items that the collection should be able to contain, before needing to allocation additional memory.
+ /// The comparer to be used for matching keys against each other, or if should be used.
+ /// The comparer to be used for detecting item changes, within the collection, or if should be used.
+ public ChangeTrackingCache(
+ Func keySelector,
+ int? capacity = null,
+ IEqualityComparer? keyComparer = null,
+ IEqualityComparer? itemComparer = null)
+ {
+ _keySelector = keySelector;
+
+ _itemComparer = itemComparer ?? EqualityComparer.Default;
+
+ _itemsByKey = (capacity is int givenCapacity)
+ ? new(
+ capacity: givenCapacity,
+ comparer: keyComparer)
+ : new(
+ comparer: keyComparer);
+
+ _changeCollector = new();
+ _isChangeCollectionEnabled = true;
+ }
+
+ ///
+ public TItem this[TKey key]
+ => _itemsByKey[key];
+
+ ///
+ public int Count
+ => _itemsByKey.Count;
+
+ ///
+ /// A flag indicating whether the collection should actually collect changes, to be retrieved by calls to .
+ /// Defaults to .
+ ///
+ ///
+ /// Note that any changes previously collected, but not captured, when this property is set to will be discarded. Otherwise, it would be possible for to generate a corrupt changes, when next called, after setting this property back to .
+ ///
+ public bool IsChangeCollectionEnabled
+ {
+ get => _isChangeCollectionEnabled;
+ set
+ {
+ if ((value is false) && (_changeCollector.Count is not 0))
+ _changeCollector.Clear();
+
+ _isChangeCollectionEnabled = value;
+ }
+ }
+
+ ///
+ /// A flag indicating whether changes have been made to the collection since its creation, or since the last call to `.
+ ///
+ public bool IsDirty
+ => _isDirty;
+
+ ///
+ /// The comparer to be used for detecting value changes, within the collection.
+ ///
+ public IEqualityComparer ItemComparer
+ => _itemComparer;
+
+ ///
+ /// The comparer to be used for matching key values against each other.
+ ///
+ public IEqualityComparer KeyComparer
+ => _itemsByKey.Comparer;
+
+ ///
+ /// The function used by the collection to identify the key values of items.
+ ///
+ public Func KeySelector
+ => _keySelector;
+
+ ///
+ public Dictionary.KeyCollection Keys
+ => _itemsByKey.Keys;
+
+ ///
+ public void Add(TItem item)
+ {
+ var key = _keySelector.Invoke(item);
+
+ _itemsByKey.Add(key, item);
+
+ if (_isChangeCollectionEnabled)
+ _changeCollector.AddChange(KeyedChange.Addition(key, item));
+
+ _isDirty = true;
+ }
+
+ ///
+ public void AddOrReplace(TItem item)
+ {
+ var key = _keySelector.Invoke(item);
+
+ var wasKeyFound = _itemsByKey.TryGetValue(key, out var oldItem);
+
+ if (wasKeyFound && _itemComparer.Equals(oldItem, item))
+ return;
+
+ _itemsByKey[key] = item;
+
+ if (_isChangeCollectionEnabled)
+ _changeCollector.AddChange(wasKeyFound
+ ? KeyedChange.Replacement(
+ key: key,
+ oldItem: oldItem!,
+ newItem: item)
+ : KeyedChange.Addition(key, item));
+
+ _isDirty = true;
+ }
+
+ ///
+ public void AddOrReplaceRange(IEnumerable values)
+ {
+ ArgumentNullException.ThrowIfNull(values, nameof(values));
+
+ if (values.TryGetNonEnumeratedCount(out var valueCount))
+ {
+ _itemsByKey.EnsureCapacity(_itemsByKey.Count + valueCount);
+
+ if (_isChangeCollectionEnabled)
+ _changeCollector.EnsureCapacity(_changeCollector.Count + valueCount);
+ }
+
+ foreach (var value in values)
+ AddOrReplace(value);
+ }
+
+ ///
+ public void AddOrReplaceRange(ReadOnlySpan values)
+ {
+ _itemsByKey.EnsureCapacity(_itemsByKey.Count + values.Length);
+
+ if (_isChangeCollectionEnabled)
+ _changeCollector.EnsureCapacity(_changeCollector.Count + values.Length);
+
+ foreach (var value in values)
+ AddOrReplace(value);
+ }
+
+ ///
+ /// Captures any previously-collected changes made to the collection, and resets the collection to a "clean" state (I.E. sets to ).
+ ///
+ /// A containing all changes made to the collection since its construction, the last call to , or since was last changed to .
+ ///
+ /// Note that this method will always return an empty changeset, when is .
+ ///
+ public KeyedChangeSet CaptureChangesAndClean()
+ {
+ _isDirty = false;
+ return _changeCollector.BuildAndClear();
+ }
+
+ ///
+ public void Clear()
+ {
+ if (_itemsByKey.Count is 0)
+ return;
+
+ if (_isChangeCollectionEnabled)
+ {
+ _changeCollector.EnsureCapacity(_itemsByKey.Count);
+
+ foreach (var item in _itemsByKey)
+ _changeCollector.AddChange(KeyedChange.Removal(item.Key, item.Value));
+
+ _changeCollector.OnSourceCleared();
+ }
+
+ _isDirty = true;
+ }
+
+ ///
+ public bool Contains(TItem item)
+ => _itemsByKey.TryGetValue(_keySelector.Invoke(item), out var existingItem)
+ && _itemComparer.Equals(item, existingItem);
+
+ ///
+ public bool ContainsKey(TKey key)
+ => _itemsByKey.ContainsKey(key);
+
+ ///
+ public void CopyTo(
+ TItem[] array,
+ int arrayIndex)
+ => _itemsByKey.Values.CopyTo(array, arrayIndex);
+
+ ///
+ public void EnsureCapacity(int capacity)
+ => _itemsByKey.EnsureCapacity(capacity);
+
+ ///
+ public Dictionary.ValueCollection.Enumerator GetEnumerator()
+ => _itemsByKey.Values.GetEnumerator();
+
+ ///
+ public bool Remove(TItem item)
+ {
+ var key = _keySelector.Invoke(item);
+
+ if (!_itemsByKey.TryGetValue(key, out var existingItem)
+ || !_itemComparer.Equals(item, existingItem))
+ return false;
+
+ if (_isChangeCollectionEnabled)
+ {
+ _changeCollector.AddChange(KeyedChange.Removal(key, item));
+ if (_itemsByKey.Count is 0)
+ _changeCollector.OnSourceCleared();
+ }
+
+ _isDirty = true;
+ return true;
+ }
+
+ ///
+ public bool Remove(TKey key)
+ {
+ if (!_itemsByKey.TryGetValue(key, out var value))
+ return false;
+
+ if (_isChangeCollectionEnabled)
+ {
+ _changeCollector.AddChange(KeyedChange.Removal(key, value));
+ if (_itemsByKey.Count is 0)
+ _changeCollector.OnSourceCleared();
+ }
+
+ _isDirty = true;
+ return true;
+ }
+
+ ///
+ public void RemoveRange(IEnumerable items)
+ {
+ if (_itemsByKey.Count is 0)
+ return;
+
+ if (_isChangeCollectionEnabled && items.TryGetNonEnumeratedCount(out var itemCount))
+ _changeCollector.EnsureCapacity(itemCount);
+
+ var wereItemsRemoved = false;
+ foreach (var item in items)
+ wereItemsRemoved |= Remove(item);
+
+ if ((_isChangeCollectionEnabled) && wereItemsRemoved && (_itemsByKey.Count is 0))
+ _changeCollector.OnSourceCleared();
+ }
+
+ ///
+ public void RemoveRange(ReadOnlySpan items)
+ {
+ if (_itemsByKey.Count is 0)
+ return;
+
+ _changeCollector.EnsureCapacity(items.Length);
+
+ var wereItemsRemoved = false;
+ foreach (var item in items)
+ wereItemsRemoved |= Remove(item);
+
+ if ((_isChangeCollectionEnabled) && wereItemsRemoved && (_itemsByKey.Count is 0))
+ _changeCollector.OnSourceCleared();
+ }
+
+ ///
+ public void RemoveRange(IEnumerable keys)
+ {
+ if (_itemsByKey.Count is 0)
+ return;
+
+ if (_isChangeCollectionEnabled && keys.TryGetNonEnumeratedCount(out var keyCount))
+ _changeCollector.EnsureCapacity(keyCount);
+
+ var wereKeysRemoved = false;
+ foreach (var key in keys)
+ wereKeysRemoved |= Remove(key);
+
+ if ((_isChangeCollectionEnabled) && wereKeysRemoved && (_itemsByKey.Count is 0))
+ _changeCollector.OnSourceCleared();
+ }
+
+ ///
+ public void RemoveRange(ReadOnlySpan keys)
+ {
+ if (_itemsByKey.Count is 0)
+ return;
+
+ _changeCollector.EnsureCapacity(keys.Length);
+
+ var wereKeysRemoved = false;
+ foreach (var key in keys)
+ wereKeysRemoved |= Remove(key);
+
+ if ((_isChangeCollectionEnabled) && wereKeysRemoved && (_itemsByKey.Count is 0))
+ _changeCollector.OnSourceCleared();
+ }
+
+ ///
+ public void Reset(IEnumerable items)
+ {
+ Clear();
+ AddOrReplaceRange(items);
+ }
+
+ ///
+ public void Reset(ReadOnlySpan values)
+ {
+ Clear();
+ AddOrReplaceRange(values);
+ }
+
+ ///
+ public bool TryGetItem(
+ TKey key,
+ [MaybeNullWhen(false)] out TItem item)
+ => _itemsByKey.TryGetValue(key, out item);
+
+ IReadOnlyCollection ICache.Keys
+ => _itemsByKey.Keys;
+
+ IReadOnlyCollection IReadOnlyCache.Keys
+ => _itemsByKey.Keys;
+
+ bool ICollection.IsReadOnly
+ => false;
+
+ IEnumerator IEnumerable.GetEnumerator()
+ => _itemsByKey.Values.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator()
+ => _itemsByKey.Values.GetEnumerator();
+}
diff --git a/src/DynamicDataVNext/Keyed/ChangeTrackingDictionary.cs b/src/DynamicDataVNext/Keyed/ChangeTrackingDictionary.cs
new file mode 100644
index 0000000..82272e0
--- /dev/null
+++ b/src/DynamicDataVNext/Keyed/ChangeTrackingDictionary.cs
@@ -0,0 +1,400 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace DynamicDataVNext;
+
+///
+/// A collection of items, with distinct keys, which tracks and allows for capturing of changes made to it, as they occur.
+///
+/// The type of the item keys in the collection.
+/// The type of the item values in the collection.
+public sealed class ChangeTrackingDictionary
+ : IExtendedDictionary,
+ IReadOnlyDictionary
+ where TKey : notnull
+{
+ private readonly KeyedChangeSet.Builder _changeCollector;
+ private readonly IEqualityComparer _valueComparer;
+ private readonly Dictionary _valuesByKey;
+
+ private bool _isChangeCollectionEnabled;
+ private bool _isDirty;
+
+ ///
+ /// Constructs a new instance of the class.
+ ///
+ /// The initial number of items that the collection should be able to contain, before needing to allocation additional memory.
+ /// The comparer to be used for matching keys against each other, or if should be used.
+ /// The comparer to be used for detecting value changes, within the collection, or if should be used.
+ public ChangeTrackingDictionary(
+ int? capacity = null,
+ IEqualityComparer? keyComparer = null,
+ IEqualityComparer? valueComparer = null)
+ {
+ _valueComparer = valueComparer ?? EqualityComparer.Default;
+
+ _valuesByKey = (capacity is int givenCapacity)
+ ? new(
+ capacity: givenCapacity,
+ comparer: keyComparer)
+ : new(
+ comparer: keyComparer);
+
+ _changeCollector = new();
+ _isChangeCollectionEnabled = true;
+ }
+
+ ///
+ public TValue this[TKey key]
+ {
+ get => _valuesByKey[key];
+ set
+ {
+ var wasKeyFound = _valuesByKey.TryGetValue(key, out var oldValue);
+
+ if (wasKeyFound && _valueComparer.Equals(oldValue, value))
+ return;
+
+ _valuesByKey[key] = value;
+
+ if (_isChangeCollectionEnabled)
+ _changeCollector.AddChange(wasKeyFound
+ ? KeyedChange.Replacement(
+ key: key,
+ oldItem: oldValue!,
+ newItem: value)
+ : KeyedChange.Addition(key, value));
+
+ _isDirty = true;
+ }
+ }
+
+ ///
+ public int Count
+ => _valuesByKey.Count;
+
+ ///
+ /// A flag indicating whether the collection should actually collect changes, to be retrieved by calls to .
+ /// Defaults to .
+ ///
+ ///
+ /// Note that any changes previously collected, but not captured, when this property is set to will be discarded. Otherwise, it would be possible for to generate a corrupt changes, when next called, after setting this property back to .
+ ///
+ public bool IsChangeCollectionEnabled
+ {
+ get => _isChangeCollectionEnabled;
+ set
+ {
+ if ((value is false) && (_changeCollector.Count is not 0))
+ _changeCollector.Clear();
+
+ _isChangeCollectionEnabled = value;
+ }
+ }
+
+ ///
+ /// A flag indicating whether changes have been made to the collection since its creation, or since the last call to `.
+ ///
+ public bool IsDirty
+ => _isDirty;
+
+ ///
+ /// The comparer to be used for matching key values against each other.
+ ///
+ public IEqualityComparer KeyComparer
+ => _valuesByKey.Comparer;
+
+ ///
+ public Dictionary.KeyCollection Keys
+ => _valuesByKey.Keys;
+
+ ///
+ /// The comparer to be used for detecting value changes, within the collection.
+ ///
+ public IEqualityComparer ValueComparer
+ => _valueComparer;
+
+ ///
+ public Dictionary.ValueCollection Values
+ => _valuesByKey.Values;
+
+ ///
+ public void Add(
+ TKey key,
+ TValue value)
+ {
+ _valuesByKey.Add(key, value);
+
+ if (_isChangeCollectionEnabled)
+ _changeCollector.AddChange(KeyedChange.Addition(key, value));
+
+ _isDirty = true;
+ }
+
+ ///
+ public void Add(KeyValuePair item)
+ => Add(item.Key, item.Value);
+
+ ///
+ public void AddOrReplaceRange(
+ IEnumerable values,
+ Func keySelector)
+ {
+ ArgumentNullException.ThrowIfNull(values, nameof(values));
+ ArgumentNullException.ThrowIfNull(keySelector, nameof(keySelector));
+
+ if (values.TryGetNonEnumeratedCount(out var valueCount))
+ {
+ _valuesByKey.EnsureCapacity(_valuesByKey.Count + valueCount);
+
+ if (_isChangeCollectionEnabled)
+ _changeCollector.EnsureCapacity(_changeCollector.Count + valueCount);
+ }
+
+ foreach (var value in values)
+ {
+ var key = keySelector.Invoke(value);
+
+ this[key] = value;
+ }
+ }
+
+ ///
+ public void AddOrReplaceRange(
+ ReadOnlySpan values,
+ Func keySelector)
+ {
+ ArgumentNullException.ThrowIfNull(keySelector, nameof(keySelector));
+
+ _valuesByKey.EnsureCapacity(_valuesByKey.Count + values.Length);
+
+ if (_isChangeCollectionEnabled)
+ _changeCollector.EnsureCapacity(_changeCollector.Count + values.Length);
+
+ foreach (var value in values)
+ {
+ var key = keySelector.Invoke(value);
+
+ this[key] = value;
+ }
+ }
+
+ ///
+ public void AddOrReplaceRange(IEnumerable> items)
+ {
+ ArgumentNullException.ThrowIfNull(items, nameof(items));
+
+ if (items.TryGetNonEnumeratedCount(out var itemCount))
+ {
+ _valuesByKey.EnsureCapacity(_valuesByKey.Count + itemCount);
+
+ if (_isChangeCollectionEnabled)
+ _changeCollector.EnsureCapacity(_changeCollector.Count + itemCount);
+ }
+
+ foreach (var item in items)
+ this[item.Key] = item.Value;
+ }
+
+ ///
+ public void AddOrReplaceRange(ReadOnlySpan> items)
+ {
+ _valuesByKey.EnsureCapacity(_valuesByKey.Count + items.Length);
+
+ if (_isChangeCollectionEnabled)
+ _changeCollector.EnsureCapacity(_changeCollector.Count + items.Length);
+
+ foreach (var item in items)
+ this[item.Key] = item.Value;
+ }
+
+ ///
+ /// Captures any previously-collected changes made to the collection, and resets the collection to a "clean" state (I.E. sets to ).
+ ///
+ /// A containing all changes made to the collection since its construction, the last call to , or since was last changed to .
+ ///
+ /// Note that this method will always return an empty changeset, when is .
+ ///
+ public KeyedChangeSet CaptureChangesAndClean()
+ {
+ _isDirty = false;
+ return _changeCollector.BuildAndClear();
+ }
+
+ ///
+ public void Clear()
+ {
+ if (_valuesByKey.Count is 0)
+ return;
+
+ if (_isChangeCollectionEnabled)
+ {
+ _changeCollector.EnsureCapacity(_valuesByKey.Count);
+
+ foreach (var item in _valuesByKey)
+ _changeCollector.AddChange(KeyedChange.Removal(item.Key, item.Value));
+
+ _changeCollector.OnSourceCleared();
+ }
+
+ _isDirty = true;
+ }
+
+ ///
+ public bool Contains(KeyValuePair item)
+ => _valuesByKey.TryGetValue(item.Key, out var value)
+ && _valueComparer.Equals(item.Value, value);
+
+ ///
+ public bool ContainsKey(TKey key)
+ => _valuesByKey.ContainsKey(key);
+
+ ///
+ public void CopyTo(
+ KeyValuePair[] array,
+ int arrayIndex)
+ => ((ICollection>)_valuesByKey).CopyTo(array, arrayIndex);
+
+ ///
+ public void EnsureCapacity(int capacity)
+ => _valuesByKey.EnsureCapacity(capacity);
+
+ ///
+ public Dictionary.Enumerator GetEnumerator()
+ => _valuesByKey.GetEnumerator();
+
+ ///
+ public bool Remove(TKey key)
+ {
+ if (!_valuesByKey.TryGetValue(key, out var value))
+ return false;
+
+ if (_isChangeCollectionEnabled)
+ {
+ _changeCollector.AddChange(KeyedChange.Removal(key, value));
+ if (_valuesByKey.Count is 0)
+ _changeCollector.OnSourceCleared();
+ }
+
+ _isDirty = true;
+ return true;
+ }
+
+ ///
+ public bool Remove(KeyValuePair item)
+ {
+ if (!_valuesByKey.Contains(item))
+ return false;
+
+ if (_isChangeCollectionEnabled)
+ {
+ _changeCollector.AddChange(KeyedChange.Removal(item.Key, item.Value));
+ if (_valuesByKey.Count is 0)
+ _changeCollector.OnSourceCleared();
+ }
+
+ _isDirty = true;
+ return true;
+ }
+
+ ///
+ public void RemoveRange(IEnumerable keys)
+ {
+ if (_valuesByKey.Count is 0)
+ return;
+
+ if (_isChangeCollectionEnabled && keys.TryGetNonEnumeratedCount(out var keyCount))
+ _changeCollector.EnsureCapacity(keyCount);
+
+ var wereKeysRemoved = false;
+ foreach (var key in keys)
+ wereKeysRemoved |= Remove(key);
+
+ if ((_isChangeCollectionEnabled) && wereKeysRemoved && (_valuesByKey.Count is 0))
+ _changeCollector.OnSourceCleared();
+ }
+
+ ///
+ public void RemoveRange(ReadOnlySpan keys)
+ {
+ if (_valuesByKey.Count is 0)
+ return;
+
+ _changeCollector.EnsureCapacity(keys.Length);
+
+ var wereKeysRemoved = false;
+ foreach (var key in keys)
+ wereKeysRemoved |= Remove(key);
+
+ if ((_isChangeCollectionEnabled) && wereKeysRemoved && (_valuesByKey.Count is 0))
+ _changeCollector.OnSourceCleared();
+ }
+
+ ///
+ public void Reset(
+ IEnumerable values,
+ Func keySelector)
+ {
+ Clear();
+ AddOrReplaceRange(values, keySelector);
+ }
+
+ ///
+ public void Reset(
+ ReadOnlySpan values,
+ Func keySelector)
+ {
+ Clear();
+ AddOrReplaceRange(values, keySelector);
+ }
+
+ ///
+ public void Reset(IEnumerable> items)
+ {
+ Clear();
+ AddOrReplaceRange(items);
+ }
+
+ ///
+ public void Reset(ReadOnlySpan> items)
+ {
+ Clear();
+ AddOrReplaceRange(items);
+ }
+
+ ///
+ public bool TryGetValue(
+ TKey key,
+ [MaybeNullWhen(false)] out TValue value)
+ => _valuesByKey.TryGetValue(key, out value);
+
+ bool ICollection>.IsReadOnly
+ => false;
+
+ ICollection IDictionary.Keys
+ => _valuesByKey.Keys;
+
+ IEnumerable IReadOnlyDictionary.Keys
+ => _valuesByKey.Keys;
+
+ IReadOnlyCollection IExtendedDictionary.Keys
+ => _valuesByKey.Keys;
+
+ ICollection IDictionary.Values
+ => _valuesByKey.Values;
+
+ IEnumerable IReadOnlyDictionary.Values
+ => _valuesByKey.Values;
+
+ IReadOnlyCollection IExtendedDictionary.Values
+ => _valuesByKey.Values;
+
+ IEnumerator IEnumerable.GetEnumerator()
+ => _valuesByKey.GetEnumerator();
+
+ IEnumerator> IEnumerable>.GetEnumerator()
+ => _valuesByKey.GetEnumerator();
+}
diff --git a/src/DynamicDataVNext/Keyed/ICache.cs b/src/DynamicDataVNext/Keyed/ICache.cs
new file mode 100644
index 0000000..2f2e299
--- /dev/null
+++ b/src/DynamicDataVNext/Keyed/ICache.cs
@@ -0,0 +1,122 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace DynamicDataVNext;
+
+///
+/// Describes a collection of distinctly-keyed items.
+///
+/// The type of the key values of items in the collection.
+/// The type of the items in the collection.
+public interface ICache
+ : ICollection
+{
+ ///
+ /// Retrieves the item in the collection for the given key.
+ ///
+ /// The key of the item to be retrieved.
+ /// Throws when does not exist within the collection.
+ /// The item in the collection with the given key.
+ TItem this[TKey key] { get; }
+
+ ///
+ /// Retrieves the current set of keys present within the collection.
+ ///
+ ///
+ /// Note that the returned collection represents a "snapshot" of the source collection, at the time at which it is created. Changes made to the source collection after a key collection is retrieved are not reflected upon the key collection.
+ ///
+ IReadOnlyCollection Keys { get; }
+
+ ///
+ /// Applies an item to the collection, as a single operation, by either adding or replacing the item, based on its key value.
+ ///
+ /// The item to be applied to the collection.
+ void AddOrReplace(TItem item);
+
+ ///
+ /// Applies a range of items to the collection, as a single operation, by either adding or replacing each item, based on its key value.
+ ///
+ /// The items to applied to the collection.
+ /// Throws for .
+ void AddOrReplaceRange(IEnumerable items);
+
+ ///
+ /// Applies a range of items to the collection, as a single operation, by either adding or replacing each item, based on its key value.
+ ///
+ /// The items to applied to the collection.
+ void AddOrReplaceRange(ReadOnlySpan items);
+
+ ///
+ /// Checks whether a given key is currently present, within the collection.
+ ///
+ /// The key to check for.
+ /// A flag indicating whether the given key is present in the collection, or not.
+ bool ContainsKey(TKey key);
+
+ ///
+ /// Removes an item from the collection, by its key.
+ ///
+ /// The key to be removed.
+ /// A flag indicating whether the given key was present in the collection, and thus removed successfully, or not.
+ bool Remove(TKey key);
+
+ ///
+ /// Removes a set of items, from the collection, as a single operation.
+ ///
+ /// The set of items to be removed.
+ /// Throws for .
+ ///
+ /// Note that the collection will silently ignore items that are not present in the collection.
+ ///
+ void RemoveRange(IEnumerable items);
+
+ ///
+ /// Removes a set of items, from the collection, as a single operation.
+ ///
+ /// The set of items to be removed.
+ ///
+ /// Note that the collection will silently ignore items that are not present in the collection.
+ ///
+ void RemoveRange(ReadOnlySpan items);
+
+ ///
+ /// Removes a set of items, by key, from the collection, as a single operation.
+ ///
+ /// The set of keys to be removed.
+ /// Throws for .
+ ///
+ /// Note that the collection will silently ignore keys that are not present in the collection.
+ ///
+ void RemoveRange(IEnumerable keys);
+
+ ///
+ /// Removes a set of items, by key, from the collection, as a single operation.
+ ///
+ /// The set of keys to be removed.
+ ///
+ /// Note that the collection will silently ignore keys that are not present in the collection.
+ ///
+ void RemoveRange(ReadOnlySpan keys);
+
+ ///
+ /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation.
+ ///
+ /// The set of items with which to populate the collection.
+ /// Throws for .
+ void Reset(IEnumerable items);
+
+ ///
+ /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation.
+ ///
+ /// The set of items with which to populate the collection.
+ void Reset(ReadOnlySpan items);
+
+ ///
+ /// Attempts to retrieve an item from the collection, by its key.
+ ///
+ /// The key whose item is to be retrieved.
+ /// The item in the collection whose key is . The default value of is assigned, if no such item is present.
+ /// A flag indicating whether an item with the given key was successfully retrieved, or not.
+ bool TryGetItem(TKey key, [MaybeNullWhen(false)] out TItem item);
+}
diff --git a/src/DynamicDataVNext/Keyed/IExtendedDictionary.cs b/src/DynamicDataVNext/Keyed/IExtendedDictionary.cs
new file mode 100644
index 0000000..31de7e3
--- /dev/null
+++ b/src/DynamicDataVNext/Keyed/IExtendedDictionary.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+
+namespace DynamicDataVNext;
+
+///
+/// Describes an extended version of , supporting range operations.
+///
+/// The type of the item keys in the collection.
+/// The type of the item values in the collection.
+///
+/// It's worth noting that the lack of an AddRange() method on this interface is intentional. Such a method would intuitively need to follow the pattern of and throw an exception when a key is presented that is already present in the collection, which would then introduce the possibility for an AddRange() operation to only be half-applied to the collection. The most appropriate thing to do would be to roll back any items already added to the collection, when the exception is thrown, which would require quite a lot of additional state tracking and item iteration. The should usually be preferred, for adding batches of items to a collection, and if key validation is desired, it can be implemented on the consumer's end.
+///
+public interface IExtendedDictionary
+ : IDictionary
+{
+ ///
+ new IReadOnlyCollection Keys { get; }
+
+ ///
+ new IReadOnlyCollection Values { get; }
+
+ ///
+ /// Applies a range of items to the collection, as a single operation, by either adding or replacing each item, based on its selected key value.
+ ///
+ /// The values to use as for each applied item.
+ /// A selector to select a value for each applied item.
+ /// Throws for and .
+ void AddOrReplaceRange(
+ IEnumerable values,
+ Func keySelector);
+
+ ///
+ /// Applies a range of items to the collection, as a single operation, by either adding or replacing each item, based on its selected key value.
+ ///
+ /// The values to use as for each applied item.
+ /// A selector to select a value for each applied item.
+ /// Throws for .
+ void AddOrReplaceRange(
+ ReadOnlySpan values,
+ Func keySelector);
+
+ ///
+ /// Applies a range of items to the collection, as a single operation, by either adding or replacing each item, based on its key value.
+ ///
+ /// The keys and values to be applied.
+ /// Throws for .
+ void AddOrReplaceRange(IEnumerable> items);
+
+ ///
+ /// Applies a range of items to the collection, as a single operation, by either adding or replacing each item, based on its key value.
+ ///
+ /// The keys and values to be applied.
+ void AddOrReplaceRange(ReadOnlySpan> items);
+
+ ///
+ /// Removes a set of items, by key, from the collection, as a single operation.
+ ///
+ /// The set of keys to be removed.
+ /// Throws for .
+ ///
+ /// Note that the collection will silently ignore keys that are not present in the collection.
+ ///
+ void RemoveRange(IEnumerable keys);
+
+ ///
+ /// Removes a set of items, by key, from the collection, as a single operation.
+ ///
+ /// The set of keys to be removed.
+ /// Throws for .
+ ///
+ /// Note that the collection will silently ignore keys that are not present in the collection.
+ ///
+ void RemoveRange(ReadOnlySpan keys);
+
+ ///
+ /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation.
+ ///
+ /// The set of items with which to populate the collection.
+ /// A selector to select a value for each new item.
+ /// Throws for and .
+ void Reset(
+ IEnumerable values,
+ Func keySelector);
+
+ ///
+ /// Resets the items within the collection to the given set of items. I.E. clears and then re-populates the collection, as a single operation.
+ ///
+ /// The set of items with which to populate the collection.
+ /// A selector to select a value for each new item.
+ /// Throws for .
+ void Reset(
+ ReadOnlySpan values,
+ Func keySelector);
+
+ ///