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
Using Extension Methods (Recommended)
// 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);
}
};