< Summary

Information
Class: MyNet.Collections.ObservableKeyedCollection<T1, T2>
Assembly: MyNet.Collections
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Collections/ObservableKeyedCollection.cs
Tag: 323_28699572109
Line coverage
76%
Covered lines: 101
Uncovered lines: 31
Coverable lines: 132
Total lines: 457
Line coverage: 76.5%
Branch coverage
52%
Covered branches: 57
Total branches: 108
Branch coverage: 52.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
.ctor(...)100%11100%
.ctor(...)83.33%66100%
get_Dictionary()100%210%
get_IsDictionaryCreated()100%11100%
get_Item(...)100%22100%
Contains(...)50%2260%
TryGetValue(...)50%3250%
TryAdd(...)66.66%66100%
Remove(...)37.5%24837.5%
ChangeItemKey(...)100%210%
ChangeItemKey(...)50%111080%
ClearItems()50%22100%
InsertItem(...)75%1212100%
InsertItemInItems(...)100%210%
AddRange(...)50%141492.3%
RemoveRange(...)0%7280%
RemoveItem(...)50%22100%
SetItem(...)30%101083.33%
EnsureDictionaryIfNeeded()75%5466.66%
CreateDictionaryNow()50%22100%
ContainsItem(...)50%66100%
AddKeyInternal(...)50%4475%
RemoveKeyInternal(...)50%22100%
CreateDictionary()100%44100%
GetDictionaryStats()50%22100%

File(s)

https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.Collections/ObservableKeyedCollection.cs

#LineLine coverage
 1// -----------------------------------------------------------------------
 2// <copyright file="ObservableKeyedCollection.cs" company="Stéphane ANDRE">
 3// Copyright (c) Stéphane ANDRE. All rights reserved.
 4// </copyright>
 5// -----------------------------------------------------------------------
 6
 7using System;
 8using System.Collections.Generic;
 9using System.Linq;
 10
 11namespace MyNet.Collections;
 12
 13/// <summary>
 14/// A keyed observable collection that maintains an internal dictionary for fast O(1) key lookups.
 15/// Optimized for performance with automatic dictionary creation and capacity management.
 16/// </summary>
 17/// <typeparam name="TKey">The type of the key for items in the collection.</typeparam>
 18/// <typeparam name="T">The type of the items in the collection.</typeparam>
 19public abstract class ObservableKeyedCollection<TKey, T> : ObservableRangeCollection<T>
 20    where TKey : notnull
 21{
 22    private readonly int _dictionaryCreationThreshold;
 23    private Dictionary<TKey, T>? _dict;
 24
 25    /// <summary>
 26    /// Initializes a new instance of the <see cref="ObservableKeyedCollection{TKey, T}"/> class.
 27    /// </summary>
 28    protected ObservableKeyedCollection()
 629        : this([])
 30    {
 631    }
 32
 33    /// <summary>
 34    /// Initializes a new instance of the <see cref="ObservableKeyedCollection{TKey, T}"/> class with the specified comp
 35    /// </summary>
 36    /// <param name="comparer">The equality comparer used to compare keys.</param>
 37    /// <param name="dictionaryCreationThreshold">Minimum items before creating dictionary. 0 = always create.</param>
 38    protected ObservableKeyedCollection(IEqualityComparer<TKey> comparer, int dictionaryCreationThreshold = 0)
 6639        : this([], comparer, dictionaryCreationThreshold)
 40    {
 6641    }
 42
 43    /// <summary>
 44    /// Initializes a new instance of the <see cref="ObservableKeyedCollection{TKey, T}"/> class that contains elements 
 45    /// </summary>
 46    /// <param name="list">The list whose elements are copied to the new collection.</param>
 47    /// <param name="comparer">The optional comparer used to compare keys.</param>
 48    /// <param name="dictionaryCreationThreshold">Minimum items before creating dictionary. 0 = always create.</param>
 49    protected ObservableKeyedCollection(IEnumerable<T> list, IEqualityComparer<TKey>? comparer = null, int dictionaryCre
 7250        : base(list)
 51    {
 7252        Comparer = comparer ?? EqualityComparer<TKey>.Default;
 7253        _dictionaryCreationThreshold = dictionaryCreationThreshold;
 54
 55        // Pre-create dictionary if we have items above threshold OR if threshold is 0
 7256        if (_dictionaryCreationThreshold == 0 || Count > _dictionaryCreationThreshold)
 57        {
 6358            CreateDictionary();
 59        }
 7260    }
 61
 62    /// <summary>
 63    /// Gets the comparer used to compare keys.
 64    /// </summary>
 65    public IEqualityComparer<TKey> Comparer { get; }
 66
 67    /// <summary>
 68    /// Gets the internal dictionary used for fast key lookups, if it has been created.
 69    /// </summary>
 070    protected IDictionary<TKey, T>? Dictionary => _dict;
 71
 72    /// <summary>
 73    /// Gets a value indicating whether gets whether the dictionary has been created.
 74    /// </summary>
 1575    protected bool IsDictionaryCreated => _dict is not null;
 76
 77    /// <summary>
 78    /// Gets the item associated with the specified key, or default if the key is not present.
 79    /// </summary>
 80    /// <param name="key">The key of the item to get.</param>
 81    /// <returns>The item associated with the specified key, or default if not found.</returns>
 82    public T? this[TKey key]
 83    {
 84        get
 85        {
 3086            ArgumentNullException.ThrowIfNull(key);
 87
 88            // Fast path: Use dictionary if available
 3089            if (_dict is not null)
 90            {
 2791                return _dict.GetValueOrDefault(key);
 92            }
 93
 94            // Slow path: Linear search
 95            // Consider creating dictionary if we're searching frequently
 396            EnsureDictionaryIfNeeded();
 97
 398            return Items.FirstOrDefault(x => Comparer.Equals(GetKeyForItem(x), key));
 99        }
 100    }
 101
 102    /// <summary>
 103    /// Determines whether the collection contains an element with the specified key.
 104    /// </summary>
 105    /// <param name="key">The key to locate in the collection.</param>
 106    /// <returns>True if an element with the key exists; otherwise false.</returns>
 107    public bool Contains(TKey key)
 108    {
 30109        ArgumentNullException.ThrowIfNull(key);
 110
 30111        if (_dict is not null)
 112        {
 30113            return _dict.ContainsKey(key);
 114        }
 115
 0116        EnsureDictionaryIfNeeded();
 0117        return Items.Any(x => Comparer.Equals(GetKeyForItem(x), key));
 118    }
 119
 120    /// <summary>
 121    /// Attempts to get the value associated with the specified key.
 122    /// </summary>
 123    /// <param name="key">The key to locate.</param>
 124    /// <param name="value">The value if found.</param>
 125    /// <returns>True if the key was found; otherwise false.</returns>
 126    public bool TryGetValue(TKey key, out T? value)
 127    {
 6128        ArgumentNullException.ThrowIfNull(key);
 129
 6130        if (_dict is not null)
 131        {
 6132            return _dict.TryGetValue(key, out value);
 133        }
 134
 0135        EnsureDictionaryIfNeeded();
 136
 0137        value = Items.FirstOrDefault(x => Comparer.Equals(GetKeyForItem(x), key));
 0138        return value is not null;
 139    }
 140
 141    /// <summary>
 142    /// Attempts to add an item to the collection if its key is not already present.
 143    /// </summary>
 144    /// <param name="item">The item to add.</param>
 145    /// <returns>True if the item was added; false if the key was null or already exists.</returns>
 146    public bool TryAdd(T item)
 147    {
 9148        var key = GetKeyForItem(item);
 9149        if (key is null) return false;
 150
 9151        EnsureDictionaryIfNeeded();
 152
 9153        if (_dict?.ContainsKey(key) == true)
 6154            return false;
 155
 3156        Add(item);
 3157        return true;
 158    }
 159
 160    /// <summary>
 161    /// Removes the item with the specified key from the collection.
 162    /// </summary>
 163    /// <param name="key">The key of the item to remove.</param>
 164    /// <returns>True if the item was found and removed; otherwise false.</returns>
 165    public bool Remove(TKey key)
 166    {
 9167        ArgumentNullException.ThrowIfNull(key);
 168
 169        // Fast path with dictionary
 9170        if (_dict is not null)
 171        {
 9172            return _dict.TryGetValue(key, out var item) && Remove(item);
 173        }
 174
 175        // Slow path without dictionary
 0176        for (var i = 0; i < Items.Count; i++)
 177        {
 0178            if (!Comparer.Equals(GetKeyForItem(Items[i]), key)) continue;
 0179            RemoveItem(i);
 0180            return true;
 181        }
 182
 0183        return false;
 184    }
 185
 186    /// <summary>
 187    /// Changes the key associated with an existing item in the collection.
 188    /// </summary>
 189    /// <param name="item">The item whose key is changing.</param>
 190    /// <param name="newKey">The new key to associate with the item.</param>
 191    protected void ChangeItemKey(T item, TKey? newKey)
 0192        => ChangeItemKey(item, newKey, default);
 193
 194    /// <summary>
 195    /// Changes the key associated with an existing item in the collection.
 196    /// </summary>
 197    /// <param name="item">The item whose key is changing.</param>
 198    /// <param name="newKey">The new key to associate with the item.</param>
 199    /// <param name="oldKey">The old key to remove. If default, GetKeyForItem is used.</param>
 200    protected void ChangeItemKey(T item, TKey? newKey, TKey? oldKey)
 201    {
 3202        if (!ContainsItem(item))
 0203            return;
 204
 205        // Use provided oldKey or get it from item
 3206        oldKey ??= GetKeyForItem(item);
 207
 3208        if (Comparer.Equals(oldKey, newKey))
 0209            return;
 210
 3211        if (newKey is not null)
 212        {
 3213            AddKeyInternal(newKey, item);
 214        }
 215
 3216        if (oldKey is not null)
 217        {
 3218            RemoveKeyInternal(oldKey);
 219        }
 3220    }
 221
 222    /// <inheritdoc />
 223    protected override void ClearItems()
 224    {
 3225        _dict?.Clear();
 3226        base.ClearItems();
 3227    }
 228
 229    /// <summary>
 230    /// When implemented in a derived class, returns the key for the specified item.
 231    /// </summary>
 232    /// <param name="item">The item to extract the key from.</param>
 233    /// <returns>The key for the specified item, or null if no key is associated.</returns>
 234    protected abstract TKey? GetKeyForItem(T item);
 235
 236    protected override void InsertItem(int index, T item)
 237    {
 360238        var key = GetKeyForItem(item);
 239
 240        // Add to base collection first
 360241        base.InsertItem(index, item);
 242
 243        // Then handle dictionary
 360244        if (key is not null)
 245        {
 246            // Check threshold AFTER item is added
 247            // Special case: if threshold is 0, always use dictionary
 357248            if (_dict is not null ||
 357249                (_dictionaryCreationThreshold == 0 && Count > 0) ||
 357250                Count > _dictionaryCreationThreshold)
 251            {
 336252                if (_dict is null)
 253                {
 3254                    CreateDictionary();
 255                }
 256                else
 257                {
 333258                    _dict.Add(key, item);
 259                }
 260            }
 261        }
 357262    }
 263
 264    /// <summary>
 265    /// Inserts an item directly into the underlying Items collection without dictionary handling.
 266    /// Use with caution - this bypasses key tracking.
 267    /// </summary>
 268    /// <param name="index">The position at which to insert the item.</param>
 269    /// <param name="item">The item to insert.</param>
 0270    protected void InsertItemInItems(int index, T item) => base.InsertItem(index, item);
 271
 272    /// <summary>
 273    /// Adds the elements of the specified collection to the end of the collection.
 274    /// Overridden to ensure dictionary is updated.
 275    /// </summary>
 276    /// <param name="items">The collection whose elements should be added.</param>
 277    public override void AddRange(IEnumerable<T> items)
 278    {
 9279        ArgumentNullException.ThrowIfNull(items);
 280
 9281        var initialCount = Count;
 282
 283        // Call parent AddRange which adds to Items directly
 9284        base.AddRange(items);
 285
 286        // Now update dictionary with all new items
 9287        if (Count > initialCount)
 288        {
 289            // Check if we should create dictionary
 9290            if (_dict is null && (_dictionaryCreationThreshold == 0 || Count > _dictionaryCreationThreshold))
 291            {
 292                // Create dictionary and it will include all items
 0293                CreateDictionary();
 294            }
 9295            else if (_dict is not null)
 296            {
 297                // Dictionary exists, add only the new items
 298                // Items were added from initialCount to Count-1
 66299                for (var i = initialCount; i < Count; i++)
 300                {
 24301                    var item = Items[i];
 24302                    var key = GetKeyForItem(item);
 24303                    if (key is not null)
 304                    {
 24305                        _dict.TryAdd(key, item); // Use TryAdd to avoid exceptions on duplicates
 306                    }
 307                }
 308            }
 309        }
 9310    }
 311
 312    /// <summary>
 313    /// Removes the items with the specified keys from the collection.
 314    /// </summary>
 315    /// <param name="keys">The keys of the items to remove.</param>
 316    /// <returns>The number of items removed.</returns>
 317    public int RemoveRange(IEnumerable<TKey> keys)
 318    {
 0319        ArgumentNullException.ThrowIfNull(keys);
 320
 0321        var set = new HashSet<TKey>(keys, Comparer);
 322
 0323        var removedCount = 0;
 324
 325        // First, remove from dictionary
 0326        if (_dict is not null)
 327        {
 0328            removedCount += set.Count(key => _dict.Remove(key));
 329        }
 330
 331        // Then, remove from base items collection
 0332        for (var i = Items.Count - 1; i >= 0; i--)
 333        {
 0334            var key = GetKeyForItem(Items[i]);
 0335            if (key is not null && set.Contains(key))
 336            {
 0337                RemoveItem(i);
 0338                removedCount++;
 339            }
 340        }
 341
 0342        return removedCount;
 343    }
 344
 345    protected override void RemoveItem(int index)
 346    {
 6347        var key = GetKeyForItem(Items[index]);
 348
 6349        base.RemoveItem(index);
 350
 6351        if (key is not null)
 352        {
 6353            RemoveKeyInternal(key);
 354        }
 6355    }
 356
 357    protected override void SetItem(int index, T item)
 358    {
 3359        var newKey = GetKeyForItem(item);
 3360        var oldKey = GetKeyForItem(Items[index]);
 361
 3362        if (Comparer.Equals(oldKey, newKey))
 363        {
 0364            if (newKey is not null && _dict is not null)
 365            {
 0366                _dict[newKey] = item;
 367            }
 368        }
 369        else
 370        {
 3371            if (newKey is not null)
 372            {
 3373                EnsureDictionaryIfNeeded();
 3374                AddKeyInternal(newKey, item);
 375            }
 376
 3377            if (oldKey is not null)
 378            {
 3379                RemoveKeyInternal(oldKey);
 380            }
 381        }
 382
 3383        base.SetItem(index, item);
 3384    }
 385
 386    /// <summary>
 387    /// Ensures the dictionary is created if the collection size warrants it.
 388    /// </summary>
 389    private void EnsureDictionaryIfNeeded()
 390    {
 15391        if (_dict is null && Count >= _dictionaryCreationThreshold)
 392        {
 0393            CreateDictionary();
 394        }
 15395    }
 396
 397    /// <summary>
 398    /// Forces creation of the dictionary regardless of size.
 399    /// </summary>
 400    protected void CreateDictionaryNow()
 401    {
 3402        if (_dict is null)
 403        {
 3404            CreateDictionary();
 405        }
 3406    }
 407
 408    private bool ContainsItem(T item)
 409    {
 3410        var key = GetKeyForItem(item);
 411
 3412        return _dict is null || key is null
 3413            ? Items.Contains(item)
 3414            : _dict.TryGetValue(key, out var itemInDict) && EqualityComparer<T>.Default.Equals(itemInDict, item);
 415    }
 416
 417    /// <summary>
 418    /// Internal add key without additional locking (assumes already locked).
 419    /// </summary>
 420    private void AddKeyInternal(TKey key, T item)
 421    {
 6422        if (_dict is null)
 423        {
 0424            CreateDictionary();
 425        }
 426
 6427        _dict?.Add(key, item);
 6428    }
 429
 430    /// <summary>
 431    /// Internal remove key without additional locking (assumes already locked).
 432    /// </summary>
 12433    private void RemoveKeyInternal(TKey key) => _dict?.Remove(key);
 434
 435    private void CreateDictionary()
 436    {
 437        // Pre-allocate with current count + some headroom
 69438        var capacity = Math.Max(Count, 16);
 69439        _dict = new(capacity, Comparer);
 440
 180441        foreach (var item in Items)
 442        {
 21443            var key = GetKeyForItem(item);
 21444            if (key is not null)
 445            {
 446                // Use TryAdd to avoid exceptions if duplicate keys exist
 21447                _dict.TryAdd(key, item);
 448            }
 449        }
 69450    }
 451
 452    /// <summary>
 453    /// Gets statistics about the dictionary usage.
 454    /// </summary>
 3455    protected (bool Created, int Count, int Capacity) GetDictionaryStats() => _dict is null ? (false, 0, 0) : (true, _di
 456}
 457