Table of Contents

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 feed directly
  • TransformationsSelect() to project values
  • StreamingFromAsyncEnumerable() for real-time data
  • MVVM Binding — Automatic PropertyChanged forwarding 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 IsPaused is 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 StateChanged otherwise

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 partial properties for automatic initialization (no manual setup required)
  • Supports init accessors: 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 ObjectDisposedException after 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