Table of Contents

KeyedListFeed

Keyed list feeds combine the benefits of list feeds with O(1) dictionary-style key-based access, enabling efficient lookup and update operations on collections.

Namespace: DevBitsLab.Feeds
Assembly: DevBitsLab.Feeds.dll


Overview

Traditional list feeds provide O(n) linear search for finding items by key. Keyed list feeds maintain an internal dictionary index that enables:

  • O(1) key-based lookup — instant access to any item by its key
  • O(1) key-based updates — modify items without linear search
  • O(1) existence checks — quickly determine if a key exists
  • Full incremental change tracking — all standard list feed capabilities

Interfaces

IKeyedListFeed<TKey, T>

The base interface for keyed list feeds.

public interface IKeyedListFeed<TKey, T> : IDisposable
    where TKey : notnull
{
    IListFeed<T> Source { get; }
    Func<T, TKey> KeySelector { get; }
    T this[TKey key] { get; }
    int Count { get; }
    IEnumerable<TKey> Keys { get; }
    bool ContainsKey(TKey key);
    bool TryGetValue(TKey key, out T value);
    int IndexOfKey(TKey key);
    void Update(TKey key, Func<T, T> transform);
    bool Remove(TKey key);
    bool Upsert(T item);
}

Creating Keyed List Feeds

From Existing List Feed (AsKeyed)

var products = ListFeed<Product>.Create(LoadProductsAsync);

// Create keyed view
using var productsById = products.AsKeyed(p => p.Id);

Standalone Keyed List Feed

// Empty keyed list
var items = ListFeed<Guid, Item>.Empty(item => item.Id);

// From existing items
var items = ListFeed<Guid, Item>.FromItems(existingItems, item => item.Id);

Keyed Incremental List Feed (WinUI)

// Combines virtualized loading with O(1) key access
var assets = IncrementalListFeed<Guid, Asset>.Create(
    loadMoreAsync: async (count, ct) =>
    {
        var items = await api.GetAssetsAsync(skip: assets.Count, take: count, ct);
        return (items, hasMore: items.Count == count);
    },
    keySelector: asset => asset.Id);

Operations

Key-Based Access

// Direct access - throws KeyNotFoundException if not found
var product = productsById["SKU-12345"];

// Safe access
if (productsById.TryGetValue("SKU-12345", out var product))
{
    Console.WriteLine(product.Name);
}

// Existence check
if (productsById.ContainsKey("SKU-12345"))
{
    // Key exists
}

// Get index of key
int index = productsById.IndexOfKey("SKU-12345");

Key-Based Updates

// O(1) update by key - no linear search!
productsById.Update("SKU-12345", p => p with { Price = p.Price * 0.9m });

// Compare with traditional approach:
// products.Update(p => p with { ... }, p => p.Id == "SKU-12345"); // O(n)!

Key-Based Removal

// O(1) remove by key
bool removed = productsById.Remove("SKU-12345");

Upsert (Insert or Update)

// Add if key doesn't exist, update if it does
bool wasUpdated = productsById.Upsert(new Product { Id = "SKU-NEW", Name = "New Product" });
// wasUpdated: false if added, true if updated existing

Performance Comparison

Operation Regular ListFeed Keyed ListFeed
Find by key O(n) O(1)
Update by key O(n) O(1)
Remove by key O(n) O(1)
Contains key O(n) O(1)
Index of key O(n) O(1)
Access by index O(1) O(1)
Add item O(1) O(1)
Remove by index O(1) O(1)

Benchmark Results

Benchmark Items Mean Allocated
Find by key (linear) 1,000 12.5 μs -
Find by key (keyed) 1,000 15 ns -
Update by key (linear) 1,000 13.2 μs -
Update by key (keyed) 1,000 18 ns -
Find by key (linear) 10,000 125 μs -
Find by key (keyed) 10,000 15 ns -

Note: Keyed access is ~830x faster for 1,000 items and ~8,300x faster for 10,000 items.


Memory Overhead

Keyed list feeds maintain a dictionary index that adds memory overhead:

Items Approximate Overhead
100 ~3 KB
1,000 ~32 KB
10,000 ~320 KB
100,000 ~3.2 MB

The overhead is approximately 24-32 bytes per item (dictionary entry + reference).


Common Patterns

Selection Tracking with SelectFrom

public partial class ViewModel : ObservableObject
{
    [BindableFeed]
    public partial IncrementalListFeed<Guid, Asset> Assets { get; init; }

    [BindableFeed]
    public partial State<Guid?> SelectedAssetId { get; init; }

    [BindableFeed]
    public partial Feed<Asset?> SelectedAsset { get; init; }

    public ViewModel()
    {
        Assets = IncrementalListFeed<Guid, Asset>
            .Create(LoadAssetsAsync, a => a.Id);

        SelectedAssetId = State<Guid?>.FromValue(null);
        
        // Auto-updates when key OR item changes
        SelectedAsset = SelectedAssetId.SelectFrom(Assets);
    }

    public void UpdateSelected()
    {
        if (SelectedAssetId is { HasValue: true, Value: { } id })
        {
            // O(1) update - SelectedAsset automatically refreshes
            Assets.Update(id, a => a with { Name = "Updated" });
        }
    }
}

Multiple Keyed Views

var products = ListFeed<Product>.Create(LoadProductsAsync);

// Different keys for different lookup patterns
using var byId = products.AsKeyed(p => p.Id);
using var bySku = products.AsKeyed(p => p.Sku);
using var byBarcode = products.AsKeyed(p => p.Barcode);

// All views stay in sync
products.Add(new Product { Id = "1", Sku = "SKU-1", Barcode = "123" });
// All three views can now access the new product

Filtered Keyed View

// Only active products are indexed
var activeProductsById = products
    .Where(p => p.IsActive)
    .AsKeyed(p => p.Id);

// Accessing an inactive product's ID throws KeyNotFoundException

Batch Operations

productsById.Source.BatchUpdate(editor =>
{
    // Batch multiple updates for a single notification
    productsById.Update("SKU-1", p => p with { Price = 10 });
    productsById.Update("SKU-2", p => p with { Price = 20 });
    productsById.Remove("SKU-3");
    editor.Add(new Product { Id = "SKU-4", Price = 30 });
});

Thread Safety

Keyed list feeds inherit thread safety from the underlying ListFeed<T>:

  • All operations are protected by internal locking
  • The dictionary index is updated atomically with list changes
  • Safe to access from multiple threads

Best Practices

  1. Dispose keyed views when no longer needed to unsubscribe from events and free memory
  2. Choose keys wisely — keys must be unique and immutable
  3. Use TryGetValue when key existence is uncertain to avoid exceptions
  4. Prefer keyed updates over index-based updates for clarity and performance
  5. Consider memory — each keyed view adds ~32 bytes per item

See Also