Table of Contents

CombinedFeed<TSource, TResult> Class

Combines multiple feeds into a single reactive feed.

Namespace: DevBitsLab.Feeds
Assembly: DevBitsLab.Feeds.dll

public sealed class CombinedFeed<TSource, TResult> : IFeed<TResult>, IDisposable

Overview

CombinedFeed enables:

  • Multi-source composition — Combine data from multiple feeds
  • Automatic updates — Result updates when any source changes
  • State aggregation — Loading/error states from all sources
  • Type-safe combiners — Compile-time type checking

Creating Combined Feeds

// Two feeds
var profileFeed = userFeed.CombineWith(
    settingsFeed,
    (user, settings) => new ProfileViewModel(user, settings));

// Three feeds
var dashboardFeed = userFeed.CombineWith(
    ordersFeed,
    metricsFeed,
    (user, orders, metrics) => new DashboardViewModel(user, orders, metrics));

Using Static Combine

// Same-type feeds
var maxFeed = CombinedFeed<int, int>.Combine(
    values => values.Max(),
    feed1, feed2, feed3);

// Different-type feeds (explicit cast needed)
var combined = CombinedFeed<object, Result>.Combine(
    values => CreateResult(values),
    (Feed<object>)(object)userFeed,
    (Feed<object>)(object)ordersFeed);

Properties

Property Type Description
Feed Feed<TResult> Underlying result feed
State FeedState Aggregated state
IsLoading bool Any source loading
HasValue bool Result available
HasError bool Any source has error
Value TResult? Combined result
Error Exception? First error
IsPaused bool Updates paused

Events

Event Description
StateChanged State transitions
PausedChanged Pause state changed

Methods

RefreshAsync

Refreshes all source feeds concurrently.

await combinedFeed.RefreshAsync();

Pause / Resume / TogglePause

Control update flow.

combinedFeed.Pause();
combinedFeed.Resume();

State Aggregation

Condition Combined State
Any loading, all have values Refreshing
Any loading, any has error Retrying
Any loading Loading
Any has error (no loading) HasError
All have values HasValue
Otherwise None

FeedCombineExtensions

Extension methods for combining feeds.

public static class FeedCombineExtensions

CombineWith (2 Feeds)

public static CombinedFeedHandle<TResult> CombineWith<T1, T2, TResult>(
    this Feed<T1> feed1,
    Feed<T2> feed2,
    Func<T1, T2, TResult> selector)

Example:

var userFeed = Feed<User>.Create(LoadUserAsync);
var settingsFeed = Feed<Settings>.Create(LoadSettingsAsync);

var combinedFeed = userFeed.CombineWith(
    settingsFeed,
    (user, settings) => new UserSettingsViewModel(user, settings));

// combinedFeed updates when either source changes

CombineWith (3 Feeds)

public static CombinedFeedHandle<TResult> CombineWith<T1, T2, T3, TResult>(
    this Feed<T1> feed1,
    Feed<T2> feed2,
    Feed<T3> feed3,
    Func<T1, T2, T3, TResult> selector)

Example:

var dashboardFeed = userFeed.CombineWith(
    ordersFeed,
    analyticsFeed,
    (user, orders, analytics) => new DashboardData {
        User = user,
        RecentOrders = orders.Take(5).ToList(),
        Stats = analytics
    });

Merge

Combine same-type feeds into a list.

public static CombinedFeed<T, List<T>> Merge<T>(params Feed<T>[] feeds)

Example:

var feed1 = Feed<int>.Create(() => Task.FromResult(1));
var feed2 = Feed<int>.Create(() => Task.FromResult(2));
var feed3 = Feed<int>.Create(() => Task.FromResult(3));

var mergedFeed = FeedCombineExtensions.Merge(feed1, feed2, feed3);
// mergedFeed.Value == [1, 2, 3]

Switch

Select between feeds based on condition.

public static Feed<T> Switch<T>(
    this Feed<bool> conditionFeed,
    Feed<T> trueFeed,
    Feed<T> falseFeed)

Example:

var isAdminFeed = Feed<bool>.Create(CheckIsAdminAsync);
var adminDashboard = Feed<Dashboard>.Create(LoadAdminDashboardAsync);
var userDashboard = Feed<Dashboard>.Create(LoadUserDashboardAsync);

var dashboardFeed = isAdminFeed.Switch(adminDashboard, userDashboard);
// Shows admin dashboard when isAdmin = true

CombinedFeedHandle<T>

Handle for combined feeds with cleanup management.

public sealed class CombinedFeedHandle<T> : IFeed<T>

Usage

// Get handle from CombineWith
var handle = userFeed.CombineWith(settingsFeed, CreateViewModel);

// Use directly (implements IFeed<T>)
if (handle.HasValue) {
    var viewModel = handle.Value;
}

// Or convert to Feed<T>
Feed<ViewModel> feed = handle;

// Dispose cleans up subscriptions
handle.Feed.Dispose();

Common Patterns

Dashboard with Multiple Sources

public class DashboardViewModel : IDisposable {
    private readonly CombinedFeedHandle<DashboardData> _dataFeed;
    
    public DashboardViewModel(
        IUserService userService,
        IOrderService orderService,
        IAnalyticsService analyticsService) {
        
        var userFeed = Feed<User>.Create(userService.GetCurrentAsync);
        var ordersFeed = Feed<List<Order>>.Create(orderService.GetRecentAsync);
        var statsFeed = Feed<Stats>.Create(analyticsService.GetStatsAsync);
        
        _dataFeed = userFeed.CombineWith(
            ordersFeed,
            statsFeed,
            (user, orders, stats) => new DashboardData(user, orders, stats));
        
        _dataFeed.StateChanged += OnStateChanged;
    }
    
    public bool IsLoading => _dataFeed.IsLoading;
    public DashboardData? Data => _dataFeed.Value;
    
    private void OnStateChanged(object? s, FeedStateChangedEventArgs e) {
        OnPropertyChanged(nameof(IsLoading));
        OnPropertyChanged(nameof(Data));
    }
    
    public async Task RefreshAsync() => await _dataFeed.RefreshAsync();
    
    public void Dispose() => _dataFeed.Feed.Dispose();
}

Conditional Data Loading

var permissionFeed = Feed<bool>.Create(CheckPermissionAsync);
var sensitiveDataFeed = Feed<SensitiveData>.CreateDeferred(LoadSensitiveDataAsync);
var publicDataFeed = Feed<PublicData>.Create(LoadPublicDataAsync);

// Only load sensitive data if permitted
permissionFeed.StateChanged += async (s, e) => {
    if (permissionFeed.HasValue && permissionFeed.Value) {
        await sensitiveDataFeed.RefreshAsync();
    }
};

Form with Dependent Data

var customerFeed = Feed<Customer>.Create(LoadCustomerAsync);
var orderHistoryFeed = Feed<List<Order>>.Create(LoadOrderHistoryAsync);
var paymentMethodsFeed = Feed<List<PaymentMethod>>.Create(LoadPaymentMethodsAsync);

var formDataFeed = customerFeed.CombineWith(
    orderHistoryFeed,
    paymentMethodsFeed,
    (customer, orders, payments) => new CustomerFormData {
        Customer = customer,
        OrderHistory = orders,
        AvailablePaymentMethods = payments,
        DefaultPaymentMethod = payments.FirstOrDefault(p => p.IsDefault)
    });

// Form loads all data in parallel
// Shows loading state until all complete

MVVM Command Integration

var dataFeed = userFeed.CombineWith(
    settingsFeed,
    CreateViewModel,
    RefreshCommand);  // Notifies command when refresh availability changes

[RelayCommand(CanExecute = nameof(CanRefresh))]
private async Task RefreshAsync() => await _dataFeed.RefreshAsync();

private bool CanRefresh() => !_dataFeed.IsLoading;

Tips

Dispose Combined Feeds

public class MyViewModel : IDisposable {
    private readonly CombinedFeedHandle<Data> _feed;
    
    public void Dispose() {
        _feed.Feed.Dispose();  // Cleans up subscriptions
    }
}

Handle Partial Data

// Show partial data while loading remaining
var combined = userFeed.CombineWith(ordersFeed, (user, orders) => 
    new ViewModel { User = user, Orders = orders });

combined.StateChanged += (s, e) => {
    if (e.NewState == FeedState.Refreshing) {
        // Show existing data with loading indicator
        ShowData(combined.Value);
        ShowRefreshIndicator();
    }
};

Error Handling

combined.StateChanged += (s, e) => {
    if (combined.HasError) {
        // Check which source failed
        if (userFeed.HasError) HandleUserError(userFeed.Error);
        if (ordersFeed.HasError) HandleOrderError(ordersFeed.Error);
    }
};

See Also