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
- Dispose keyed views when no longer needed to unsubscribe from events and free memory
- Choose keys wisely — keys must be unique and immutable
- Use
TryGetValuewhen key existence is uncertain to avoid exceptions - Prefer keyed updates over index-based updates for clarity and performance
- Consider memory — each keyed view adds ~32 bytes per item
See Also
- ListFeed<T> — Base list feed implementation
- ListFeedExtensions — AsKeyed and SelectFrom extensions
- WinUI Integration — SelectionHelper for keyed incremental feeds