ListFeedExtensions Class
LINQ-like operations for list feeds with incremental change tracking.
Namespace: DevBitsLab.Feeds
Assembly: DevBitsLab.Feeds.dll
public static class ListFeedExtensions
Overview
Unlike standard LINQ, these extensions create reactive list feeds that:
- Subscribe to source
ItemsChangedevents - Propagate changes incrementally when possible
- Maintain their own change notifications
Operations
| Method | Description |
|---|---|
Select |
Project items |
Where |
Filter items |
OrderBy |
Sort ascending |
OrderByDescending |
Sort descending |
Take |
First N items |
Skip |
Skip N items |
Distinct |
Remove duplicates |
Aggregate |
Compute aggregate value |
AsFeed |
Convert to Feed<IReadOnlyList<T>> |
Chaining Example
var products = ListFeed<Product>.Create(LoadProductsAsync);
// Create a filtered, sorted, projected view
var activeProductNames = products
.Where(p => p.IsActive)
.OrderBy(p => p.Name)
.Select(p => p.Name);
// Updates incrementally when products changes
Select
Project each element into a new form.
public static ListFeed<TResult> Select<TSource, TResult>(
this IListFeed<TSource> source,
Func<TSource, TResult> selector)
public static ListFeed<TResult> Select<TSource, TResult>(
this IListFeed<TSource> source,
Func<TSource, int, TResult> selector) // With index
| Source Change | Result Change |
|---|---|
| Add | Add projected item |
| Remove | Remove item |
| Update | Re-project, update |
| Move | Move (no re-project) |
Example:
var customerFeed = ListFeed<Customer>.Create(LoadCustomersAsync);
// Project to view models
var viewModels = customerFeed.Select(c => new CustomerViewModel(c));
// Project to display strings
var displayNames = customerFeed.Select(c => $"{c.FirstName} {c.LastName}");
// With index (for numbered lists)
var numbered = customerFeed.Select((c, i) => $"{i + 1}. {c.Name}");
Note: Index-based selector rebuilds on structural changes (add/remove/move).
Where
Filter items by predicate.
public static ListFeed<T> Where<T>(
this IListFeed<T> source,
Func<T, bool> predicate)
| Source Change | Result Behavior |
|---|---|
| Add (matches) | Add to result |
| Add (no match) | Ignored |
| Remove (was included) | Remove from result |
| Update | Add/remove/update based on match |
Example:
var orders = ListFeed<Order>.Create(LoadOrdersAsync);
// Active orders only
var activeOrders = orders.Where(o => o.Status == OrderStatus.Active);
// High-value orders
var highValue = orders.Where(o => o.Total > 1000);
// Complex condition
var urgentPending = orders.Where(o =>
o.Status == OrderStatus.Pending &&
o.DueDate < DateTime.Today.AddDays(3));
OrderBy
Sort items in ascending order.
public static ListFeed<T> OrderBy<T, TKey>(
this IListFeed<T> source,
Func<T, TKey> keySelector)
| Source Change | Result Behavior |
|---|---|
| Add | Insert at sorted position |
| Remove | Remove from result |
| Update | Reposition if key changed |
| Move | Ignored (sorted order prevails) |
Example:
var employees = ListFeed<Employee>.Create(LoadEmployeesAsync);
// Sort by name
var byName = employees.OrderBy(e => e.LastName);
// Sort by date
var byHireDate = employees.OrderBy(e => e.HireDate);
OrderByDescending
Sort items in descending order.
public static ListFeed<T> OrderByDescending<T, TKey>(
this IListFeed<T> source,
Func<T, TKey> keySelector)
Example:
var sales = ListFeed<Sale>.Create(LoadSalesAsync);
// Highest sales first
var topSales = sales.OrderByDescending(s => s.Amount);
// Most recent first
var recentFirst = sales.OrderByDescending(s => s.Date);
Take
Return first N items.
public static ListFeed<T> Take<T>(
this IListFeed<T> source,
int count)
Example:
var notifications = ListFeed<Notification>.Create(LoadNotificationsAsync);
// Show only first 5
var top5 = notifications.Take(5);
// Top 10 sales (combine with sort)
var top10Sales = sales
.OrderByDescending(s => s.Amount)
.Take(10);
Skip
Bypass first N items.
public static ListFeed<T> Skip<T>(
this IListFeed<T> source,
int count)
Example:
var results = ListFeed<SearchResult>.Create(SearchAsync);
// Pagination: page 2 (items 21-40)
var page2 = results.Skip(20).Take(20);
// Skip header row
var dataRows = csvRows.Skip(1);
Distinct
Remove duplicate items.
public static ListFeed<T> Distinct<T>(
this IListFeed<T> source)
public static ListFeed<T> Distinct<T>(
this IListFeed<T> source,
IEqualityComparer<T> comparer)
Example:
var tags = ListFeed<string>.Create(LoadAllTagsAsync);
// Remove duplicates
var uniqueTags = tags.Distinct();
// Case-insensitive distinct
var uniqueIgnoreCase = tags.Distinct(StringComparer.OrdinalIgnoreCase);
Aggregate
Compute a value from the list.
Standard Aggregate
Updates on state changes (Reset/Refresh only).
public static Feed<TResult> Aggregate<TSource, TResult>(
this IListFeed<TSource> source,
Func<IReadOnlyList<TSource>, TResult> aggregator)
Example:
var orders = ListFeed<Order>.Create(LoadOrdersAsync);
// Total value
var totalFeed = orders.Aggregate(list => list.Sum(o => o.Total));
// Count
var countFeed = orders.Aggregate(list => list.Count);
// Average
var avgFeed = orders.Aggregate(list =>
list.Count > 0 ? list.Average(o => o.Total) : 0);
AggregateWithItemChanges
Updates on every item change.
public static Feed<TResult> AggregateWithItemChanges<TSource, TResult>(
this IListFeed<TSource> source,
Func<IReadOnlyList<TSource>, TResult> aggregator)
Example:
var cart = ListFeed<CartItem>.Empty();
// Live cart total - updates on every add/remove
var cartTotal = cart.AggregateWithItemChanges(items =>
items.Sum(i => i.Price * i.Quantity));
Warning: May impact performance with large lists or frequent updates.
AsFeed
Convert to Feed<IReadOnlyList<T>>.
public static Feed<IReadOnlyList<T>> AsFeed<T>(
this IListFeed<T> source)
Use case: Combining with other feeds.
var products = ListFeed<Product>.Create(LoadProductsAsync);
var category = Feed<Category>.Create(LoadCategoryAsync);
// Need Feed<T> for CombineWith
var combined = products.AsFeed().CombineWith(
category,
(productList, cat) => new CategoryProductsViewModel(cat, productList));
Common Patterns
Filtered & Sorted View
var products = ListFeed<Product>.Create(LoadProductsAsync);
var displayProducts = products
.Where(p => p.IsAvailable)
.OrderBy(p => p.Category)
.ThenBy(p => p.Name); // Note: ThenBy not available, use composite key
// Alternative: composite key for multi-sort
var sorted = products
.OrderBy(p => (p.Category, p.Name));
Pagination
const int pageSize = 20;
int currentPage = 0;
var allItems = ListFeed<Item>.Create(LoadAllItemsAsync);
var currentPageItems = allItems
.Skip(currentPage * pageSize)
.Take(pageSize);
void NextPage() {
currentPage++;
// Need to recreate derived feed with new page
}
Search Results
var allProducts = ListFeed<Product>.Create(LoadProductsAsync);
string searchTerm = "";
var searchResults = allProducts
.Where(p => string.IsNullOrEmpty(searchTerm) ||
p.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase))
.OrderBy(p => p.Name);
Summary Statistics
var orders = ListFeed<Order>.Create(LoadOrdersAsync);
// Multiple aggregates
var orderCount = orders.Aggregate(list => list.Count);
var totalValue = orders.Aggregate(list => list.Sum(o => o.Total));
var avgValue = orders.Aggregate(list =>
list.Count > 0 ? list.Average(o => o.Total) : 0m);
// Combine into summary
var summaryFeed = orderCount.CombineWith(
totalValue,
avgValue,
(count, total, avg) => new OrderSummary {
OrderCount = count,
TotalValue = total,
AverageValue = avg
});
Real-time Dashboard
var transactions = ListFeed<Transaction>.Create(LoadTransactionsAsync);
// Live updates on every transaction
var liveTotal = transactions.AggregateWithItemChanges(t =>
t.Sum(x => x.Amount));
var liveCount = transactions.AggregateWithItemChanges(t => t.Count);
var recentTransactions = transactions
.OrderByDescending(t => t.Timestamp)
.Take(10);
Performance Notes
- Select: O(1) per change (except indexed version)
- Where: O(log n) per change using optimized index mapping
- OrderBy: O(log n) insert using binary search, cached keys
- Take/Skip: Rebuilds on any change (O(n))
- Distinct: Rebuilds on any change (O(n))
- Aggregate: O(n) per update
- AsKeyed: O(1) key lookup, O(1) update by key
- SelectFrom: O(1) per key/item change
AsKeyed
Create a keyed view over a list feed for O(1) key-based access.
public static KeyedListFeedView<TKey, T> AsKeyed<TKey, T>(
this IListFeed<T> source,
Func<T, TKey> keySelector)
where TKey : notnull
Returns: KeyedListFeedView<TKey, T> implementing IKeyedListFeed<TKey, T>
Example:
var products = ListFeed<Product>.Create(LoadProductsAsync);
// Create keyed view
using var productsById = products.AsKeyed(p => p.Id);
// O(1) lookup by key
var product = productsById["SKU-12345"];
// O(1) update by key - no keySelector needed!
productsById.Update("SKU-12345", p => p with { Price = p.Price * 0.9m });
// O(1) check if exists
if (productsById.ContainsKey("SKU-99999"))
{
Console.WriteLine("Found!");
}
// Safe lookup
if (productsById.TryGetValue("SKU-789", out var p))
{
Console.WriteLine(p.Name);
}
Performance:
| Operation | Complexity |
|---|---|
this[key] |
O(1) |
ContainsKey |
O(1) |
TryGetValue |
O(1) |
Update(key, ...) |
O(1) |
Remove(key) |
O(1) |
IndexOfKey |
O(1) |
Multiple keys on same source:
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 with the source
With filtering:
// Only active products are indexed
var activeProductsById = products
.Where(p => p.IsActive)
.AsKeyed(p => p.Id);
Note: Remember to
Dispose()keyed views when no longer needed.
SelectFrom
Create a feed that selects an item from a keyed list, automatically updating when either the key changes or the item at that key changes.
// For non-nullable key:
public static Feed<T?> SelectFrom<TKey, T>(
this IFeed<TKey> keyFeed,
IKeyedListFeed<TKey, T> keyedList)
where TKey : notnull
// For nullable key (Nullable<T>):
public static Feed<T?> SelectFrom<TKey, T>(
this IFeed<TKey?> keyFeed,
IKeyedListFeed<TKey, T> keyedList)
where TKey : struct
The Problem
When using immutable records and with expressions, derived feeds don't update when an item is replaced:
// This doesn't update when the item at the key changes!
SelectedAsset = SelectedAssetId.Select(id => id.HasValue ? Assets[id.Value] : null);
// Update the item:
Assets.Update(selectedId, asset => asset with { Name = "Updated" });
// SelectedAsset still references the OLD asset instance!
The Solution
// SelectFrom updates when:
// 1. SelectedAssetId changes (different item selected)
// 2. The item at SelectedAssetId is updated/replaced in Assets
SelectedAsset = SelectedAssetId.SelectFrom(Assets);
// Now when you update:
Assets.Update(selectedId, asset => asset with { Name = "Updated" });
// SelectedAsset automatically gets the new instance!
Complete Example
public partial class MainViewModel : 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 MainViewModel()
{
Assets = IncrementalListFeed<Guid, Asset>
.Create(LoadAssetsAsync, asset => asset.Id);
SelectedAssetId = State<Guid?>.FromValue(null);
// Automatic updates on key change OR item change
SelectedAsset = SelectedAssetId.SelectFrom(Assets);
}
[RelayCommand]
public void UpdateAsset()
{
if (SelectedAssetId is { HasValue: true, Value: { } selectedId })
{
// O(1) update - SelectedAsset automatically gets the new instance
Assets.Update(selectedId, asset => asset with { Name = "Updated" });
// No need to manually re-trigger SelectedAsset!
}
}
}
How It Works
SelectFrom subscribes to both:
- Key feed state changes — triggers re-evaluation when the key changes
- List feed item changes — triggers re-evaluation when the item at the current key is added, updated, or removed
Incremental behavior:
| Change | SelectFrom Behavior |
|---|---|
| Key changes | Re-evaluates, returns new item |
| Item at key updated | Re-evaluates, returns new item |
| Item at key removed | Returns default |
| Item at key added | Re-evaluates, returns item |
| Unrelated item changes | No re-evaluation |
| List reset | Re-evaluates |
Comparison with Select
| Approach | Key Changes | Item Changes |
|---|---|---|
.Select(id => list[id]) |
✓ Updates | ✗ No update |
.SelectFrom(list) |
✓ Updates | ✓ Updates |
Performance
- Creation: O(1)
- Key change: O(1) dictionary lookup
- Item change detection: O(1) key comparison
- Memory: ~40 bytes overhead per SelectFrom instance
See Also
IListFeed<T>— List feed interfaceListFeed<T>— List feed implementationListChange<T>— Change types- WinUI Integration — SelectionHelper for WinUI controls