Feeds architecture

This document provides information about the architecture and implementation of feeds. To learn about how to use the feeds framework in your applications, refer to the feeds concepts documentation.

Dev general guidelines

  • All instances of feeds should be cached using the AttachedProperty helper class. This helper ensures that each feed instance is cached and reused based on an owner and key.
  • Consequently, all feed implementations must be stateless (except for the special case of State<T>).
  • When invoking a user's asynchronous method, the SourceContext should be set as ambient (see FeedHelper.InvokeAsync). The SourceContext manages the subscription state and caching for feeds.
  • Untyped interfaces (such as IMessage, IMessageEntry, etc.) exist only for data binding purposes and should not be re-implemented or used in source code.

Caching

To enable a lightweight creation syntax in property getters (such as public Feed<int> MyFeed => _anotherFeed.Select(_ => 42), which is re-evaluated each time the property is accessed), while avoiding rebuilding and re-querying the feed on every access, the framework implements two levels of caching.

Instance caching

The first level caches the feed instance itself using the AttachedProperty helper class. Each feed is attached to an owner and identified by a key. This ensures that for a given owner, there is only one instance of the declared feed. For example, calling _anotherFeed.Select(_ => 42) multiple times will always return the same feed instance.

The owner is determined by the following order of preference:

  1. The parent feed, if one exists (such as _anotherFeed in the example above)
  2. The Target property of the key delegate, which is the instance of the class declaring the feed
  3. The key delegate itself, if it's a static delegate instance (for example, in _ => 42, the Target will be null because the delegate doesn't capture any variables)

The key is usually the delegate that is provided by the user. It’s really important here to note that a helper method like below would break the instance caching:

public static Feed<string> ToString(this IFeed<T> feed, Func<T, string> toString)
  => feed.Select(t => toString(t));

As the delegate t => toString(t) is declared in the method itself, it will be re-instantiated each time. Valid implementations would be:

public static Feed<string> ToString(this IFeed<T> feed, Func<T, string> toString)
  => feed.Select(toString); // We are directly forwarding the user delegate
public static Feed<string> ToString(this IFeed<T> feed, Func<T, string> toString)
  => AttachedProperty.GetOrCreate(owner: feed, key: toString, factory: (theFeed, theToString) => theFeed.Select(t => theToString(t))); // We are explicitly caching the instance.

Subscription caching

The second level of caching manages the "subscription" to a feed. This ensures that within a given context, enumerating or awaiting the same feed multiple times won't rebuild the value (notably, it won't re-request a value from a web API call).

This caching is achieved by the SourceContext.

These context instances are weakly attached to an owner (typically a ViewModel), and each call to context.GetOrCreateSource(feed) returns a stateful subscription to the feed that replays the last received message.

Important

When implementing an IFeed, the context provided in GetSource is only intended to be used to restore it as the current context in specific circumstances, such as when invoking a user's async method. Your feed must remain stateless, so you should not call context.GetOrCreateSource(parent).

On the other hand, any helper that allows users to "subscribe" to a feed should use SourceContext.Current.GetOrCreateSource(feed) rather than feed.GetSource(SourceContext.Current).

Issuing messages

When implementing an IFeed, you need to create and yield messages to subscribers.

If your feed doesn't have a parent feed, the easiest approach is to start with Message<T>.Initial (do not send it as the first message), and then update it:

var current = Message<int>.Initial;
for (var i = 0; i++; i < 42)
{
  yield return i;
  await Task.Delay(100, ct);
}

If your feed does have a parent feed, you should use the MessageManager<TParent, TResult> class, for example:

var manager = new MessageManager<TParent, TResult>();
var msgIndex = 0;
await foreach(var parentMsg in _parent.GetSource(context))
{
  if (manager.Update(localMsg => localMsg.With(parentMsg).Data(msgIndex++)))
  {
    yield return manager.Current;
  }
}
Important

Make sure that your feed always produces at least one message. If there isn't any relevant message to send, yield Message<T>.Initial before completing the IAsyncEnumerable source.

If you have a parent feed, make sure to always forward the parent message, even if it doesn't change any local value: manager.Update(localMsg => localMsg.With(parentMsg)).

Note

Enumeration of an IAsyncEnumerable is sequential (i.e., one value at a time). The MessageManager also has a constructor that allows asynchronously sending messages to an AsyncEnumerableSubject.

Axes

An axe is referring to an “informational axe” related to a given data, a.k.a. a metadata. Currently the feed framework is managing (i.e., actively generating value for) only 2 metadata: error and progress, but as Messages are designed to encapsulate a data and all its metadata, a MessageEntry can have more than those 2 well-known axes.

┌────┐1   *┌────────┐    2┌────────────┐1   *┌────────────────┐
│Feed├────►│Message │  ┌─►│MessageEntry├────►│MessageAxis     │
└────┘     ├────────┤  │  ├────────────┤     └─────┬──────────┘
           │Previous├──┤  │Data        │1          │1
           │Current ├──┘  │Error       ├──┐        │
           │Changes │     │Progress    │  │        │1
           └────────┘     └────────────┘  │ *┌─────┴──────────┐
                                          └─►│MessageAxisValue│
                                             └────────────────┘

MessageEntry are basically dictionaries of MessageAxis and MessageAxisValue, except they are returning MessageAxisValue.Unset instead of throwing error for unset axes.

The DataAxis is a special axis that must be set on all entries. It exists only to unify/ease implementation. You should use Option.Undefined to “unset” the data.

If you are about to add an axe, you should make sure to provided extensions methods over IMessageBuilder and IMessageEntry to read/write it directly to/from the effective metadata type. The generic Get and Set are there for that and should not be used directly in user’s code.

Request

The subscriber of a feed can send some request to the source feed to enhance its behavior. The most common request is the RefreshRequest.

When implementing an IFeed you have access to those requests using the Requests<TRequest>() method on the SourceContext you get in the GetSource.

When consuming a feed, you can send a request to that feed by creating a "child" context (SourceContext.CreateChild()) giving you own IRequestSource.

Token and TokenSet

In a response to a request a feed might issue a token that is then added to its messages so the subscriber that sent the request is able to track the completion of the request. This is the case for the Refresh and the Pagination which are forwarding those tokens through the refresh and the pagintation axes.

As when a subscriber sends a request, it might be handled by more than one feed. For instance, when combining two AsyncFeed instances, the refresh request will cause those two feeds to refresh their data. Even if it's not yet the implemented, we can also imagine that an operator feed (such as the SelectAsyncFeed) might also trigger a refresh of its own projection. Refresh and Pagination axes are working with TokenSet. It is a collection of IToken that only keep the latest tokens for a given source in relation to the subscription context.

        Subscriber       Combine     Async A     Async B

            │               │           │           │
       ┌─ Control channel (Requests on SourceContext) ─┐
       │    │               │           │           │  │
       │    │   Request 1   │           │           │  │
    ┌──┼──◄─┼──────────────►│ Request 1 │           │  │
    │  │    │               ├──────────►│           │  │
    ▼  │    │               │  Token A  │           │  │
    │  │    │               │◄──────────┘           │  │
    │  │    │               │       Request 1       │  │
    ▼  │    │               ├──────────────────────►│  │
    │  │    │               │        Token B        │  │
    │  │    │               │◄──────────┬───────────┤  │
    ▼  │    │ TokenSet[A,B] │           │           │  │
    │  │    │◄──────────────┤           │           │  │
    │  │    │               │           │           │  │
    ▼  └────┼───────────────┼───────────┼───────────┼──┘
IsExecuting │               │           │           │
    │  ┌─ Message channel ──┼───────────┼───────────┼──┐
    │  │    │               │           │           │  │
    ▼  │    │               ┤ Msg with  │           │  │
    │  │    │               │  token A  │           │  │
    │  │    │               │◄──────────┤ Msg with  │  │
    ▼  │    │   Msg with    │           │  token A  │  │
    │  │    │ TokenSet[A,B] │◄──────────┼───────────┤  │
    └─►├───►│◄──────────────┤           │           │  │
       │    │               │           │           │  │
       └────┼───────────────┼───────────┼───────────┼──┘
            │               │           │           │
Note

As a subscriber, you can use the TokenSetAwaiter to wait for a set that has been produced by a request.

Note

When implementing an IFeed you can use the CoercingRequestManager or the SequentialRequestManager to easily implement such request / token logic.

When you answer to a request with a token, you must then issue a new message with that token.

View

While developing in the feed framework, the most interesting view-related part is the presentation layer. Feeds are transferring messages containing the data and its metadata, but those are not binding friendly. The presentation layer has then 2 responsibilities:

  1. Read, i.e. from the ViewModel to the View, it will expose and maintain the state of the last message through standard binding-friendly properties and INotifyPropertyChanged;
  2. Write, i.e. from the View to the ViewModel, it will convert back standard properties into so called IInput, which inherits from the IFeed interface, and which allows the ViewModel to manage the view just like any other data source.

Those binding friendly properties are generated by the FeedsGenerator.

To trigger the generation, the class needs to have at least on constructor which has an IInput, or an ICommandBuilder parameter, or flag the type with [ReactiveBindable(true)] attribute.

When we generate a binding friendly class for a type MyType we are generating a BindableMyType class nested into the MyType itself. This class inherits from the BindableViewModelBase.

Denormalizing

In order to make a record editable field per field (e.g. a user profile where you want to edit first name and last name independently), we can generate a BindableRecord object that we re-expose each property of the Record independently.

When a record is denormalized, we are generating / maintaining only one State<Record>. Setting a value of any of the denormalized properties, will update that root state.

When a record is denormalized, we can no longer directly set a record instance on the VM.Record (the property is of type BindableRecord). To work around this, if the record does not have a Value property which would conflict, we are generating a get/set property Value which can be used to set the instance (e.g. VM.Record.Value).

When a property is updated, we also ensure to raise the property changes, including for the Value property (if defined) and the GetValue method.

Dispatching and threading considerations

The user’s ViewModel are expected to run on background thread for performance considerations. Commands and state updates from generated ViewModel classes are always ran on a background thread.

To avoid strong dependency on a dispatcher (neither by injection nor by the need to create VM on UI thread), all generated classes are auto resolving the dispatcher when an event handler is registered or when a public method is used, using the internal LazyDispatcherResolver and EventManager. Those classes are then completing their initialization once the first dispatcher is resolved.

The EventManager will capture the thread used to register an event handler and will then make sure to invoke that handler on that thread if it was a UI thread. This allows BindableVM to be data-bound to multiple window/UI threads.

┌───────────────┐     ┌───────────────┐
│EventManager   │     │IInvocationList│
├───────────────┤1   *├───────────────┤
│Add(handler)   ├────►│Add(handler)   │
│Remove(handler)│     │Remove(handler)│
│Raise(args)    │     │Raise(args)    │
└───────────────┘     └───────▲───────┘
                              │
              ┌───────────────┴──────────────┐
              │                              │
  ┌───────────┴────────────┐    ┌────────────┴────────────┐
  │DispatcherInvocationList│    │MultiThreadInvocationList│
  └───────────▲────────────┘    └─────────────────────────┘
              │
        ┌─────┴───────┐
        │             │
  ┌─────┴─────┐   ┌───┴────┐
  │Coaslescing│   │Queueing│
  └───────────┘   └────────┘

We have one IInvocationList per UI thread and one for all background threads (the MultiThreadInvocationList).

As a bonus, when a dispatcher bound handler is about to be invoked from another thread, it can be coalesced to be raised only once (cf. CoalescingDispatcherInvocationList).