Feed<T> Class
Reactive data feed with automatic loading, state tracking, and transformation support.
Namespace: DevBitsLab.Feeds
Assembly: DevBitsLab.Feeds.dll
public sealed class Feed<T> : IFeed<T>, IDisposable
Overview
Feed<T> is the core data primitive for managing single asynchronous values. It provides:
- Auto-loading on creation (or deferred loading)
- State management — Loading, HasValue, HasError
- Awaitable — Use
await feeddirectly - Transformations —
Select()to project values - Streaming —
FromAsyncEnumerable()for real-time data - MVVM Binding — Automatic
PropertyChangedforwarding with[BindableFeed]
Creating Feeds
Create (Auto-loading)
Starts loading immediately upon creation.
var feed = Feed<Customer>.Create(async ct => {
return await customerApi.GetAsync(customerId, ct);
});
// Feed is already loading
Console.WriteLine(feed.IsLoading); // true
CreateDeferred (Manual Loading)
Waits for explicit RefreshAsync() call.
var feed = Feed<Report>.CreateDeferred(async ct => {
return await reportService.GenerateAsync(parameters, ct);
});
// Later, when user clicks "Generate"
await feed.RefreshAsync();
FromValue (Pre-populated)
Creates a feed already in HasValue state.
var cached = cache.Get<User>("current_user");
var feed = Feed<User>.FromValue(cached);
Console.WriteLine(feed.HasValue); // true
FromError (Error State)
Creates a feed in HasError state.
var feed = Feed<User>.FromError(
new UnauthorizedAccessException("Session expired"));
FromAsyncEnumerable (Streaming)
Creates a feed from a stream of values.
// Stream stock prices
var priceFeed = Feed<StockPrice>.FromAsyncEnumerable(
stockService.StreamPricesAsync(symbol));
// Or with a factory for refreshable streams
var priceFeed = Feed<StockPrice>.FromAsyncEnumerable(ct =>
stockService.StreamPricesAsync(symbol, ct));
// Refresh restarts the stream
await priceFeed.RefreshAsync();
Properties
| Property | Type | Description |
|---|---|---|
State |
FeedState |
Current state flags |
IsLoading |
bool |
Data is being fetched |
HasValue |
bool |
Data loaded successfully |
HasError |
bool |
Error occurred |
Value |
T? |
Current value |
Error |
Exception? |
Error details |
IsPaused |
bool |
Updates ignored when true |
Methods
RefreshAsync
Reloads data from the source. Cancels any in-progress operation.
await customerFeed.RefreshAsync();
UpdateValue
Directly sets the value without invoking the load function.
// Optimistic update
orderFeed.UpdateValue(orderFeed.Value! with { Status = "Submitted" });
// Then confirm with server
try {
await orderService.SubmitOrderAsync(orderId);
} catch {
await orderFeed.RefreshAsync(); // Revert on failure
}
Note: Ignored when
IsPausedis true.
Select<TResult>
Creates a reactive feed by transforming the value. The projected feed automatically updates when the source feed changes.
var customerFeed = Feed<Customer>.Create(LoadCustomerAsync);
// Project to a display string
var displayNameFeed = customerFeed.Select(c =>
$"{c.FirstName} {c.LastName}");
// When customerFeed updates, displayNameFeed updates automatically
customerFeed.UpdateValue(new Customer { FirstName = "Jane", LastName = "Doe" });
// displayNameFeed.Value is now "Jane Doe"
Reactive behavior:
- Mirrors the source feed's loading/error states
- Automatically applies the selector when the source value changes
- Propagates errors from both the source feed and the selector function
- Unsubscribes from source when disposed
Cancel
Cancels any in-progress load without triggering a new one.
slowFeed.Cancel();
Pause / Resume / TogglePause
Controls update acceptance.
feed.Pause(); // Ignore UpdateValue calls
feed.Resume(); // Accept updates again
bool paused = feed.TogglePause();
Awaiting Feeds
Feeds are directly awaitable using GetAwaiter().
var feed = Feed<User>.Create(LoadUserAsync);
try {
User user = await feed;
Console.WriteLine($"User: {user.Name}");
} catch (Exception ex) {
Console.WriteLine($"Failed: {ex.Message}");
}
Behavior:
- Returns immediately if
HasValue - Throws immediately if
HasError - Waits for
StateChangedotherwise
Events
StateChanged
feed.StateChanged += (sender, e) => {
Console.WriteLine($"State: {e.OldState} → {e.NewState}");
};
PausedChanged
feed.PausedChanged += (sender, isPaused) => {
pauseButton.Text = isPaused ? "Resume" : "Pause";
};
MVVM Integration
Command Notifications
Create feeds with ICommand notifications:
// Command's CanExecute updates when feed finishes loading
var feed = Feed<Data>.Create(LoadDataAsync, RefreshCommand);
var deferred = Feed<Data>.CreateDeferred(LoadDataAsync, LoadCommand);
[BindableFeed] Attribute
Use the [BindableFeed] attribute to enable automatic PropertyChanged forwarding when the feed's value changes:
public partial class CustomerViewModel : ObservableObject
{
[BindableFeed]
public partial Feed<Customer> Customer { get; set; }
[BindableFeed]
public partial Feed<string> DisplayName { get; set; }
public CustomerViewModel()
{
Customer = Feed<Customer>.Create(LoadCustomerAsync);
DisplayName = Customer.Select(c => $"{c.FirstName} {c.LastName}");
}
}
Key points:
- Use
partialproperties for automatic initialization (no manual setup required) - Supports
initaccessors:public partial Feed<T> Prop { get; init; } - Works with any class inheriting from
ObservableObject
XAML Binding:
<TextBlock Text="{x:Bind ViewModel.Customer.Value.Name, Mode=OneWay}" />
<TextBlock Text="{x:Bind ViewModel.DisplayName.Value, Mode=OneWay}" />
<ProgressRing IsActive="{x:Bind ViewModel.Customer.IsLoading, Mode=OneWay}" />
💡 Tip: The included analyzers will warn you if you forget the
[BindableFeed]attribute on feed properties in ViewModels.
Common Patterns
Pull-to-Refresh
async Task OnPullToRefreshAsync()
{
refreshIndicator.IsRefreshing = true;
try
{
await dataFeed.RefreshAsync();
}
finally
{
refreshIndicator.IsRefreshing = false;
}
}
Error Retry with Exponential Backoff
public class RetryingFeed<T> : IDisposable
{
private readonly Feed<T> _feed;
private int _retryCount;
private const int MaxRetries = 3;
public RetryingFeed(Func<CancellationToken, Task<T>> loadFunc)
{
_feed = Feed<T>.Create(loadFunc);
_feed.StateChanged += OnStateChanged;
}
private async void OnStateChanged(object? sender, FeedStateChangedEventArgs e)
{
if (e.NewState == FeedState.HasError && _retryCount < MaxRetries)
{
_retryCount++;
var delay = TimeSpan.FromSeconds(Math.Pow(2, _retryCount)); // 2, 4, 8 seconds
await Task.Delay(delay);
await _feed.RefreshAsync();
}
else if (e.NewState == FeedState.HasValue)
{
_retryCount = 0; // Reset on success
}
}
public void Dispose()
{
_feed.StateChanged -= OnStateChanged;
_feed.Dispose();
}
}
Simple Retry (Limited Attempts)
int retryCount = 0;
const int maxRetries = 3;
feed.StateChanged += async (s, e) =>
{
if (e.NewState == FeedState.HasError && retryCount < maxRetries)
{
retryCount++;
await Task.Delay(TimeSpan.FromSeconds(2 * retryCount));
await feed.RefreshAsync();
}
else if (e.NewState == FeedState.HasValue)
{
retryCount = 0;
}
};
Combining Feeds
var profileFeed = userFeed.CombineWith(
settingsFeed,
(user, settings) => new ProfileViewModel(user, settings));
See CombinedFeed for details.
Lifecycle & Disposal
public class CustomerViewModel : IDisposable {
private readonly Feed<Customer> _customerFeed;
public CustomerViewModel(int customerId) {
_customerFeed = Feed<Customer>.Create(ct =>
_api.GetCustomerAsync(customerId, ct));
}
public void Dispose() {
_customerFeed.Dispose();
}
}
Disposal behavior:
- Cancels in-progress operations
- Cleans up event subscriptions
- Methods throw
ObjectDisposedExceptionafter disposal
Performance Notes
| Operation | Approx. Time | Allocations |
|---|---|---|
Access Value |
~0.002 ns | 0 |
Access HasValue |
~0.23 ns | 0 |
UpdateValue |
~1.95 ns | 0 |
Pause/Resume |
~1.97 ns | 0 |
CreateDeferred |
~20 ns | 120 B |
FromValue |
~34 ns | 208 B |
await (cached) |
~40 ns | 176 B |
Select |
~82 ns | 360 B |
Thread Safety
Feed<T> is thread-safe for all operations:
// Safe from any thread
await Task.Run(() => feed.UpdateValue(newValue));
await Task.Run(() => feed.RefreshAsync());
// Concurrent access is safe
await Task.WhenAll(
Task.Run(() => Console.WriteLine(feed.Value)),
Task.Run(() => feed.UpdateValue(value1)),
Task.Run(() => feed.UpdateValue(value2))
);
Thread Safety Guarantees
| Operation | Thread-Safe |
|---|---|
Property access (Value, State, etc.) |
✅ Yes |
UpdateValue |
✅ Yes |
RefreshAsync |
✅ Yes |
Pause / Resume |
✅ Yes |
Dispose |
✅ Yes |
| Event subscription | ✅ Yes |
Event Handler Threading
Important: Event handlers (
StateChanged,PausedChanged) are invoked on the thread that triggered the state change. If updating UI, marshal to the UI thread:
feed.StateChanged += (s, e) => {
// WPF
Dispatcher.Invoke(() => UpdateUI());
// WinUI / MAUI
DispatcherQueue.TryEnqueue(() => UpdateUI());
};
See Also
IFeed<T>— Interface definitionFeedState— State enumerationState<T>— Editable feed with validationCombinedFeed— Combining feeds- Analyzers — Code analysis rules for best practices