DevBitsLab.Feeds
0.0.10
dotnet add package DevBitsLab.Feeds --version 0.0.10
NuGet\Install-Package DevBitsLab.Feeds -Version 0.0.10
<PackageReference Include="DevBitsLab.Feeds" Version="0.0.10" />
<PackageVersion Include="DevBitsLab.Feeds" Version="0.0.10" />
<PackageReference Include="DevBitsLab.Feeds" />
paket add DevBitsLab.Feeds --version 0.0.10
#r "nuget: DevBitsLab.Feeds, 0.0.10"
#:package DevBitsLab.Feeds@0.0.10
#addin nuget:?package=DevBitsLab.Feeds&version=0.0.10
#tool nuget:?package=DevBitsLab.Feeds&version=0.0.10
DevBitsLab.Feeds
A reactive data management library for .NET that provides state-aware feeds with automatic loading, error handling, and incremental change tracking for collections.
📖 Overview
DevBitsLab.Feeds is a reactive data management library that simplifies working with asynchronous data in .NET applications. It provides a unified approach to handling loading states, errors, and data updates through a composable feed abstraction.
Key Benefits
- Simplified State Management: Automatic tracking of loading, error, and value states
- Reactive Updates: Event-driven architecture for real-time UI synchronization
- Incremental Collection Updates: Efficient list change tracking for optimal UI performance
- Type-Safe Composition: Combine multiple data sources with compile-time type checking
- MVVM-Ready: Built-in support for ICommand integration and data binding
- Built-in Analyzers: Roslyn analyzers to enforce best practices
📚 Documentation
📖 Full API Documentation — Detailed guides for all classes and interfaces
| Quick Links | |
|---|---|
| Feed<T> | Single-value reactive feed |
| ListFeed<T> | Reactive list with change tracking |
| State<T> | Editable state with validation |
| CombinedFeed | Combine multiple feeds |
| Analyzers | Roslyn analyzers reference |
📋 Requirements
- .NET 10.0 or later
- C# 14 (Preview)
- Nullable Reference Types enabled
📦 Installation
.NET CLI
dotnet add package DevBitsLab.Feeds
Package Manager Console
Install-Package DevBitsLab.Feeds
🚀 Getting Started
Basic Feed Usage
using DevBitsLab.Feeds;
// Create a feed that loads data from an API
var customerFeed = Feed<Customer>.Create(async ct =>
{
return await httpClient.GetFromJsonAsync<Customer>($"/api/customers/{id}", ct);
});
// Subscribe to state changes
customerFeed.StateChanged += (sender, e) =>
{
if (customerFeed.IsLoading)
ShowLoadingSpinner();
else if (customerFeed.HasValue)
DisplayCustomer(customerFeed.Value!);
else if (customerFeed.HasError)
ShowError(customerFeed.Error!.Message);
};
// Or await the feed directly
var customer = await customerFeed;
List Feed with Change Tracking
var productsFeed = ListFeed<Product>.Create(async ct =>
{
return await productApi.GetProductsAsync(ct);
});
// Subscribe to incremental changes
productsFeed.ItemsChanged += (sender, e) =>
{
switch (e.Change)
{
case ListChange<Product>.ItemAdded added:
InsertRowAt(added.Index, added.Item);
break;
case ListChange<Product>.ItemRemoved removed:
RemoveRowAt(removed.Index);
break;
}
};
// Modify the list - changes are tracked automatically
productsFeed.Add(new Product { Name = "New Product" });
Editable State with Validation
var settingsState = State<Settings>.Create(
loadFunc: async ct => await settingsService.LoadAsync(ct),
saveFunc: async (settings, ct) => await settingsService.SaveAsync(settings, ct),
validateFunc: settings =>
{
if (string.IsNullOrWhiteSpace(settings?.Username))
return ValidationResult.Error(nameof(Settings.Username), "Username is required");
return ValidationResult.Success;
});
// Edit and save
settingsState.Update(settingsState.Value! with { Theme = "Dark" });
if (settingsState.IsDirty && !settingsState.HasErrors)
await settingsState.SaveAsync();
🔧 Core Types
| Type | Description |
|---|---|
Feed<T> |
Reactive feed for single values with async loading |
ListFeed<T> |
Reactive list with incremental change notifications |
State<T> |
Editable wrapper with dirty tracking and validation |
ValidationResult |
Property-level validation errors |
CombinedFeed |
Combines multiple feeds into one |
🔗 Standard Interface Support
| Interface | Type | Description |
|---|---|---|
INotifyCollectionChanged |
ListFeed<T> |
Native WPF/WinUI ItemsSource binding |
INotifyPropertyChanged |
All feeds | Property change notifications |
INotifyDataErrorInfo |
State<T> |
Validation error binding |
IObservable<T> |
All feeds | Rx interop via .ToObservable() |
ISupportIncrementalLoading |
IncrementalListFeed<T>* |
WinUI virtualized loading |
*Available in DevBitsLab.Feeds.WinUI package
📌 Feed States
| State | Description |
|---|---|
None |
Initial state - no data loaded |
Loading |
Currently fetching data |
HasValue |
Data successfully loaded |
HasError |
Error occurred during loading |
Refreshing |
Updating existing data |
Retrying |
Retrying after error |
📚 API Quick Reference
Feed Methods
Create(loadFunc)- Create auto-loading feedCreateDeferred(loadFunc)- Create manually-triggered feedFromValue(value)- Create feed with existing valueFromError(exception)- Create feed in error stateRefreshAsync()- Reload data from sourceSelect(selector)- Transform feed valueToObservable()- Convert toIObservable<T>for Rx
ListFeed Methods
Empty()- Create empty list feedFromItems(items)- Create from existing itemsAdd(item)/Insert(index, item)- Add itemsRemove(item)/RemoveAt(index)- Remove itemsUpdate(index, newItem)- Replace itemBatchUpdate(action)- Atomic multi-operationToObservable()- Convert toIObservable<IReadOnlyList<T>>
State Methods
Update(value)- Set new value (marks dirty)SaveAsync()- Persist changesRevertAsync()- Discard changesReset(value)- Set value without dirty flagToObservable()- Convert toIObservable<T>for Rx
Feed Extension Methods
Select(selector)- Project valuesFilter(predicate)- Filter updates by predicateDebounce(delay)- Debounce value changesDistinctUntilChanged()- Suppress duplicate valuesTimeout(timeout)- Apply timeout to loadingCatch(fallback)- Handle errors with fallbackDefaultIfNull(value)- Provide default for null values
🔍 Analyzers
DevBitsLab.Feeds includes Roslyn analyzers to help enforce best practices:
| ID | Severity | Description |
|---|---|---|
| FEED001 | Warning | Missing InitializeBindableFeeds() call in constructor |
| FEED002 | Info | Unnecessary InitializeBindableFeeds() call (partial properties don't need it) |
| FEED003 | Warning | Feed property should be readonly |
| FEED004 | Info | Consider using partial property for automatic lazy binding |
| FEED005 | Warning | Missing [BindableFeed] attribute on Feed/State/ListFeed property |
| FEED006 | Info | Debounce without UI context may cause threading issues |
| FEED007 | Info | Identity Select is unnecessary (.Select(x => x)) |
| FEED008 | Warning | Filter predicate is constant |
See the full analyzer documentation for details.
✅ Best Practices
1. Use [BindableFeed] for Automatic Disposal
The [BindableFeed] attribute automatically implements IDisposable and disposes all marked feeds:
public partial class MyViewModel : ObservableObject
{
[BindableFeed]
public partial Feed<Customer> Customer { get; init; }
[BindableFeed]
public partial State<Settings> Settings { get; init; }
[BindableFeed]
public partial ListFeed<Order> Orders { get; init; }
// IDisposable is auto-generated! Just call Dispose() when done.
}
// Usage:
var viewModel = new MyViewModel { ... };
// ... use the view model ...
viewModel.Dispose(); // All feeds are automatically disposed!
If you already implement IDisposable, call DisposeBindableFeeds() from your Dispose method:
public partial class MyViewModel : ObservableObject, IDisposable
{
[BindableFeed]
public Feed<Customer> Customer { get; } = Feed<Customer>.Create(LoadAsync);
private readonly HttpClient _httpClient = new();
public MyViewModel()
{
InitializeBindableFeeds(); // Required for non-partial properties
}
public void Dispose()
{
DisposeBindableFeeds(); // Disposes Customer automatically
_httpClient.Dispose(); // Your custom cleanup
}
}
2. Batch Multiple List Changes
// Good: Single notification
listFeed.BatchUpdate(editor =>
{
editor.Add(item1);
editor.Add(item2);
});
// Avoid: Multiple notifications
listFeed.Add(item1);
listFeed.Add(item2);
3. Combine Feeds for Derived Data
var dashboardFeed = userFeed.CombineWith(
ordersFeed,
(user, orders) => new Dashboard(user, orders));
4. Use Partial Properties for Automatic Binding (C# 13+)
Partial properties enable lazy initialization without needing InitializeBindableFeeds():
public partial class MyViewModel : ObservableObject
{
[BindableFeed]
public partial Feed<Person> Person { get; set; }
// No InitializeBindableFeeds() needed - binding is set up lazily on first access
}
📊 Benchmark Results
Benchmarks run nightly. Run configuration: .NET 10, Release mode, Ubuntu Latest.
Feed Throughput
| Operation | Mean | Throughput | Allocated |
|---|---|---|---|
| Access Feed.Value | 0.002 ns | ~500B ops/sec | - |
| Access Feed.HasValue | 0.23 ns | ~4.4B ops/sec | - |
| Feed.UpdateValue | 1.95 ns | ~513M ops/sec | - |
| Create deferred Feed | 20.19 ns | ~50M ops/sec | 120 B |
| Create Feed.FromValue | 33.92 ns | ~29M ops/sec | 208 B |
| Feed.Select transformation | 82.05 ns | ~12M ops/sec | 360 B |
Key Performance Characteristics
- Zero-allocation reads: Accessing
Value,State,IsDirty,HasValueallocates nothing - Sub-nanosecond property access: Core read operations complete in under 1 ns
- Efficient updates: Single-item operations complete in nanoseconds
- Scalable collections:
ListFeed<T>operations scale well with collection size
🤝 Contributing
Contributions are welcome! Please feel free to submit issues and pull requests.
- Clone the repository
- Open in Visual Studio 2022 or later
- Build the solution
- Run tests
📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on DevBitsLab.Feeds:
| Package | Downloads |
|---|---|
|
DevBitsLab.Feeds.WinUI
WinUI extensions for DevBitsLab.Feeds including FeedPresenter control for automatic feed state presentation and ISupportIncrementalLoading support for virtualized list loading. |
GitHub repositories
This package is not used by any popular GitHub repositories.
Initial release of DevBitsLab.Feeds with Feed<T>, ListFeed<T>, State<T>, and CombinedFeed support. Supports .NET 8 and 10.